diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index 60b642e19..3490936b4 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -12,9 +12,6 @@ on: - dev - unstable - dev-unstable - - docker - - dev-docker - - dev-unstable-docker runner: type: choice description: Runner @@ -48,7 +45,7 @@ on: - next/* env: - NODEJS_VERSION: "18.15.0" + NODEJS_VERSION: "20.16.0" ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' jobs: @@ -74,24 +71,32 @@ jobs: sudo mount -t tmpfs tmpfs . if: ${{ github.event.inputs.runner == 'fast' }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-node@v3 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODEJS_VERSION }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + - name: Set up docker QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up system dependencies + run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container squashfuse - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Make run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: compiled-${{ matrix.arch }}.tar path: compiled-${{ matrix.arch }}.tar @@ -140,10 +145,19 @@ jobs: }')[matrix.platform] }} steps: - - uses: actions/checkout@v3 + - name: Free space + run: rm -rf /opt/hostedtoolcache* + if: ${{ github.event.inputs.runner != 'fast' }} + + - uses: actions/checkout@v4 with: submodules: recursive + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies run: | sudo apt-get update @@ -162,7 +176,7 @@ jobs: if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }} - name: Download compiled artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: compiled-${{ env.ARCH }}.tar @@ -171,9 +185,29 @@ jobs: - name: Prevent rebuild of compiled artifacts run: | + mkdir -p web/node_modules mkdir -p web/dist/raw + mkdir -p core/startos/bindings + mkdir -p sdk/base/lib/osBindings + mkdir -p container-runtime/node_modules + mkdir -p container-runtime/dist + mkdir -p container-runtime/dist/node_modules + mkdir -p core/startos/bindings + mkdir -p sdk/dist + mkdir -p sdk/baseDist + mkdir -p patch-db/client/node_modules + mkdir -p patch-db/client/dist + mkdir -p web/.angular + mkdir -p web/dist/raw/ui + mkdir -p web/dist/raw/install-wizard + mkdir -p web/dist/raw/setup-wizard + mkdir -p web/dist/static/ui + mkdir -p web/dist/static/install-wizard + mkdir -p web/dist/static/setup-wizard PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar + - run: git status + - name: Run iso build run: PLATFORM=${{ matrix.platform }} make iso if: ${{ matrix.platform != 'raspberrypi' }} @@ -182,18 +216,18 @@ jobs: run: PLATFORM=${{ matrix.platform }} make img if: ${{ matrix.platform == 'raspberrypi' }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.squashfs path: results/*.squashfs - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.iso path: results/*.iso if: ${{ matrix.platform != 'raspberrypi' }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.img path: results/*.img diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c6082ac25..3f47a65a4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ on: - next/* env: - NODEJS_VERSION: "18.15.0" + NODEJS_VERSION: "20.16.0" ENVIRONMENT: dev-unstable jobs: @@ -19,11 +19,11 @@ jobs: name: Run Automated Tests runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODEJS_VERSION }} diff --git a/.gitignore b/.gitignore index d33151e91..766d876e8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ secrets.db /ENVIRONMENT.txt /GIT_HASH.txt /VERSION.txt -/eos-*.tar.gz /*.deb /target /*.squashfs @@ -28,4 +27,5 @@ secrets.db /dpkg-workdir /compiled.tar /compiled-*.tar -/firmware \ No newline at end of file +/firmware +/tmp \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..c3c555a05 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,134 @@ +# Setting up your development environment on Debian/Ubuntu + +A step-by-step guide + +> This is the only officially supported build environment. +> MacOS has limited build capabilities and Windows requires [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) + +## Installing dependencies + +Run the following commands one at a time + +```sh +sudo apt update +sudo apt install -y ca-certificates curl gpg build-essential +curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list +sudo apt update +sudo apt install -y sed grep gawk jq gzip brotli containerd.io docker-ce docker-ce-cli docker-compose-plugin qemu-user-static binfmt-support squashfs-tools git debspawn rsync b3sum +sudo mkdir -p /etc/debspawn/ +echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml +sudo usermod -aG docker $USER +sudo su $USER +docker run --privileged --rm tonistiigi/binfmt --install all +docker buildx create --use +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source ~/.bashrc +nvm install 20 +nvm use 20 +nvm alias default 20 # this prevents your machine from reverting back to another version +``` + +## Cloning the repository + +```sh +git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/minor +cd start-os +``` + +## Building an ISO + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make iso +``` + +This will build an ISO for your current architecture. If you are building to run on an architecture other than the one you are currently on, replace `$(uname -m)` with the correct platform for the device (one of `aarch64`, `aarch64-nonfree`, `x86_64`, `x86_64-nonfree`, `raspberrypi`) + +## Creating a VM + +### Install virt-manager + +```sh +sudo apt update +sudo apt install -y virt-manager +sudo usermod -aG libvirt $USER +sudo su $USER +``` + +### Launch virt-manager + +```sh +virt-manager +``` + +### Create new virtual machine + +![Select "Create a new virtual machine"](assets/create-vm/step-1.png) +![Click "Forward"](assets/create-vm/step-2.png) +![Click "Browse"](assets/create-vm/step-3.png) +![Click "+"](assets/create-vm/step-4.png) + +#### make sure to set "Target Path" to the path to your results directory in start-os + +![Create storage pool](assets/create-vm/step-5.png) +![Select storage pool](assets/create-vm/step-6.png) +![Select ISO](assets/create-vm/step-7.png) +![Select "Generic or unknown OS" and click "Forward"](assets/create-vm/step-8.png) +![Set Memory and CPUs](assets/create-vm/step-9.png) +![Create disk](assets/create-vm/step-10.png) +![Name VM](assets/create-vm/step-11.png) +![Create network](assets/create-vm/step-12.png) + +## Updating a VM + +The fastest way to update a VM to your latest code depends on what you changed: + +### UI or startd: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-startbox REMOTE=start9@ +``` + +### Container runtime or debian dependencies: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-deb REMOTE=start9@ +``` + +### Image recipe: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-squashfs REMOTE=start9@ +``` + +--- + +If the device you are building for is not available via ssh, it is also possible to use `magic-wormhole` to send the relevant files. + +### Prerequisites: + +```sh +sudo apt update +sudo apt install -y magic-wormhole +``` + +As before, the fastest way to update a VM to your latest code depends on what you changed. Each of the following commands will return a command to paste into the shell of the device you would like to upgrade. + +### UI or startd: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole +``` + +### Container runtime or debian dependencies: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-deb +``` + +### Image recipe: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-squashfs +``` diff --git a/Makefile b/Makefile index 65d4d79dd..2f3e17bdb 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ BASENAME := $(shell ./basename.sh) PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi) ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) -BINS := core/target/$(ARCH)-unknown-linux-gnu/release/startbox core/target/aarch64-unknown-linux-musl/release/container-init core/target/x86_64-unknown-linux-musl/release/container-init -WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard +WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html +COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) DEBIAN_SRC := $(shell git ls-files debian/) @@ -16,17 +16,17 @@ STARTD_SRC := core/startos/startd.service $(BUILD_SRC) COMPAT_SRC := $(shell git ls-files system-images/compat/) UTILS_SRC := $(shell git ls-files system-images/utils/) BINFMT_SRC := $(shell git ls-files system-images/binfmt/) -CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE) -WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules web/config.json patch-db/client/dist web/patchdb-ui-seed.json +CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE) +WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) -WEB_DIAGNOSTIC_UI_SRC := $(shell git ls-files web/projects/diagnostic-ui) WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard) PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client) GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) -COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar -ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console; fi') $(PLATFORM_FILE) +COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs +ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) +REBUILD_TYPES = 1 ifeq ($(REMOTE),) mkdir = mkdir -p $1 @@ -49,10 +49,13 @@ endif .DELETE_ON_ERROR: -.PHONY: all metadata install clean format sdk snapshots uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole test +.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime registry all: $(ALL_TARGETS) +touch: + touch $(ALL_TARGETS) + metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) sudo: @@ -62,6 +65,7 @@ clean: rm -f system-images/**/*.tar rm -rf system-images/compat/target rm -rf core/target + rm -rf core/startos/bindings rm -rf web/.angular rm -f web/config.json rm -rf web/node_modules @@ -74,6 +78,13 @@ clean: rm -rf image-recipe/deb rm -rf results rm -rf build/lib/firmware + rm -rf container-runtime/dist + rm -rf container-runtime/node_modules + rm -f container-runtime/*.squashfs + if [ -d container-runtime/tmp/combined ] && mountpoint container-runtime/tmp/combined; then sudo umount container-runtime/tmp/combined; fi + if [ -d container-runtime/tmp/lower ] && mountpoint container-runtime/tmp/lower; then sudo umount container-runtime/tmp/lower; fi + rm -rf container-runtime/tmp + (cd sdk && make clean) rm -f ENVIRONMENT.txt rm -f PLATFORM.txt rm -f GIT_HASH.txt @@ -82,18 +93,29 @@ clean: format: cd core && cargo +nightly fmt -test: $(CORE_SRC) $(ENVIRONMENT_FILE) - cd core && cargo build && cargo test +test: | test-core test-sdk test-container-runtime + +test-core: $(CORE_SRC) $(ENVIRONMENT_FILE) + ./core/run-tests.sh + +test-sdk: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts + cd sdk && make test + +test-container-runtime: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json + cd container-runtime && npm test -sdk: - cd core && ./install-sdk.sh +cli: + cd core && ./install-cli.sh + +registry: + cd core && ./build-registrybox.sh deb: results/$(BASENAME).deb debian/control: build/lib/depends build/lib/conflicts ./debuild/control.sh -results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) +results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS) PLATFORM=$(PLATFORM) ./dpkg-build.sh $(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE) @@ -104,17 +126,17 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S ./image-recipe/run-local-build.sh "results/$(BASENAME).deb" # For creating os images. DO NOT USE -install: $(ALL_TARGETS) +install: $(ALL_TARGETS) $(call mkdir,$(DESTDIR)/usr/bin) - $(call cp,core/target/$(ARCH)-unknown-linux-gnu/release/startbox,$(DESTDIR)/usr/bin/startbox) + $(call mkdir,$(DESTDIR)/usr/sbin) + $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk) - $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-deno) - $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/avahi-alias) - $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/embassy-cli) - if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi - if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi + if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi + if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi + $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs) + $(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs) $(call mkdir,$(DESTDIR)/lib/systemd/system) $(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service) @@ -122,20 +144,17 @@ install: $(ALL_TARGETS) $(call mkdir,$(DESTDIR)/usr/lib) $(call rm,$(DESTDIR)/usr/lib/startos) $(call cp,build/lib,$(DESTDIR)/usr/lib/startos) + $(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime) + $(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs) $(call cp,PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt) $(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt) $(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt) $(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt) - $(call mkdir,$(DESTDIR)/usr/lib/startos/container) - $(call cp,core/target/aarch64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.arm64) - $(call cp,core/target/x86_64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.amd64) - $(call mkdir,$(DESTDIR)/usr/lib/startos/system-images) $(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar) $(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar) - $(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/binfmt.tar) $(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware) @@ -148,31 +167,103 @@ update-overlay: $(ALL_TARGETS) $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM) $(call ssh,"sudo systemctl start startd") -wormhole: core/target/$(ARCH)-unknown-linux-gnu/release/startbox - @wormhole send core/target/$(ARCH)-unknown-linux-gnu/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }' +wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox + @echo "Paste the following command into the shell of your StartOS server:" + @echo + @wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }' + +wormhole-deb: results/$(BASENAME).deb + @echo "Paste the following command into the shell of your StartOS server:" + @echo + @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' + +wormhole-squashfs: results/$(BASENAME).squashfs + $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs | head -c 32)) + $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) + @echo "Paste the following command into the shell of your StartOS server:" + @echo + @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && cd /media/startos/images && wormhole receive --accept-file %s && mv $(BASENAME).squashfs $(SQFS_SUM).rootfs && ln -rsf ./$(SQFS_SUM).rootfs ../config/current.rootfs && sync && reboot'"'"'\n", $$3 }' update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi - $(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/") - $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM) - $(call ssh,'sudo NO_SYNC=1 /media/embassy/next/usr/lib/startos/scripts/chroot-and-upgrade "apt-get install -y $(shell cat ./build/lib/depends)"') + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') + +update-startbox: core/target/$(ARCH)-unknown-linux-musl/release/startbox # only update binary (faster than full update) + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,/media/startos/next/usr/bin/startbox) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true') + +update-deb: results/$(BASENAME).deb # better than update, but only available from debian + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(call mkdir,/media/startos/next/tmp/startos-deb) + $(call cp,results/$(BASENAME).deb,/media/startos/next/tmp/startos-deb/$(BASENAME).deb) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y --reinstall /tmp/startos-deb/$(BASENAME).deb"') + +update-squashfs: results/$(BASENAME).squashfs + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs)) + $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) + $(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)') + $(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs) + $(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs') + $(call ssh,'sudo reboot') emulate-reflash: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi - $(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/") - $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM) - $(call ssh,"sudo touch /media/embassy/config/upgrade && sudo rm -f /media/embassy/config/disk.guid && sudo sync && sudo reboot") + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) + $(call ssh,'sudo rm -f /media/startos/config/disk.guid /media/startos/config/overlay/etc/hostname') + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') upload-ota: results/$(BASENAME).squashfs TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh +container-runtime/debian.$(ARCH).squashfs: + ARCH=$(ARCH) ./container-runtime/download-base-image.sh + +container-runtime/node_modules/.package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json + npm --prefix container-runtime ci + touch container-runtime/node_modules/.package-lock.json + +sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi) + mkdir -p sdk/base/lib/osBindings + rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/ + touch sdk/base/lib/osBindings/index.ts + +core/startos/bindings/index.ts: $(shell git ls-files core) $(ENVIRONMENT_FILE) + rm -rf core/startos/bindings + ./core/build-ts.sh + ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts + npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts + touch core/startos/bindings/index.ts + +sdk/dist/package.json sdk/baseDist/package.json: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts + (cd sdk && make bundle) + touch sdk/dist/package.json + touch sdk/baseDist/package.json + +# TODO: make container-runtime its own makefile? +container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json + npm --prefix container-runtime run build + +container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/install-dist-deps.sh + ./container-runtime/install-dist-deps.sh + touch container-runtime/dist/node_modules/.package-lock.json + +container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo + ARCH=$(ARCH) ./container-runtime/update-image.sh + build/lib/depends build/lib/conflicts: build/dpkg-deps/* build/dpkg-deps/generate.sh $(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE) ./download-firmware.sh $(PLATFORM) -system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) core/Cargo.lock +system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC) @@ -181,45 +272,49 @@ system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC) system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC) cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar -snapshots: core/snapshot-creator/Cargo.toml - cd core/ && ARCH=aarch64 ./build-v8-snapshot.sh - cd core/ && ARCH=x86_64 ./build-v8-snapshot.sh +core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) + ARCH=$(ARCH) ./core/build-startbox.sh + touch core/target/$(ARCH)-unknown-linux-musl/release/startbox -$(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) - cd core && ARCH=$(ARCH) ./build-prod.sh - touch $(BINS) +core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE) + ARCH=$(ARCH) ./core/build-containerbox.sh + touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox -web/node_modules: web/package.json +web/node_modules/.package-lock.json: web/package.json sdk/baseDist/package.json npm --prefix web ci + touch web/node_modules/.package-lock.json + +web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json web/node_modules/.package-lock.json + rm -rf web/.angular + mkdir -p web/.angular + touch web/.angular/.updated -web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) +web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:ui + touch web/dist/raw/ui/index.html -web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) +web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:setup + touch web/dist/raw/setup-wizard/index.html -web/dist/raw/diagnostic-ui: $(WEB_DIAGNOSTIC_UI_SRC) $(WEB_SHARED_SRC) - npm --prefix web run build:dui - -web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) +web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:install-wiz + touch web/dist/raw/install-wizard/index.html -web/dist/static: $(WEB_UIS) $(ENVIRONMENT_FILE) +$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE) ./compress-uis.sh web/config.json: $(GIT_HASH_FILE) web/config-sample.json jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json -web/patchdb-ui-seed.json: web/package.json - jq '."ack-welcome" = $(shell jq '.version' web/package.json)' web/patchdb-ui-seed.json > ui-seed.tmp - mv ui-seed.tmp web/patchdb-ui-seed.json - -patch-db/client/node_modules: patch-db/client/package.json +patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json npm --prefix patch-db/client ci + touch patch-db/client/node_modules/.package-lock.json -patch-db/client/dist: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules - ! test -d patch-db/client/dist || rm -rf patch-db/client/dist - npm --prefix web run build:deps +patch-db/client/dist/index.js: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules/.package-lock.json + rm -rf patch-db/client/dist + npm --prefix patch-db/client run build + touch patch-db/client/dist/index.js # used by github actions compiled-$(ARCH).tar: $(COMPILED_TARGETS) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) @@ -231,8 +326,11 @@ uis: $(WEB_UIS) # this is a convenience step to build the UI ui: web/dist/raw/ui -cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep: +cargo-deps/aarch64-unknown-linux-musl/release/pi-beep: ARCH=aarch64 ./build-cargo-dep.sh pi-beep -cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console: - ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console \ No newline at end of file +cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console: + ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console + +cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs: + ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs diff --git a/assets/create-vm/step-1.png b/assets/create-vm/step-1.png new file mode 100644 index 000000000..2dfafc25f Binary files /dev/null and b/assets/create-vm/step-1.png differ diff --git a/assets/create-vm/step-10.png b/assets/create-vm/step-10.png new file mode 100644 index 000000000..bc1985394 Binary files /dev/null and b/assets/create-vm/step-10.png differ diff --git a/assets/create-vm/step-11.png b/assets/create-vm/step-11.png new file mode 100644 index 000000000..322dd5394 Binary files /dev/null and b/assets/create-vm/step-11.png differ diff --git a/assets/create-vm/step-12.png b/assets/create-vm/step-12.png new file mode 100644 index 000000000..52f14f56e Binary files /dev/null and b/assets/create-vm/step-12.png differ diff --git a/assets/create-vm/step-2.png b/assets/create-vm/step-2.png new file mode 100644 index 000000000..020f3d7d1 Binary files /dev/null and b/assets/create-vm/step-2.png differ diff --git a/assets/create-vm/step-3.png b/assets/create-vm/step-3.png new file mode 100644 index 000000000..0295ba6bc Binary files /dev/null and b/assets/create-vm/step-3.png differ diff --git a/assets/create-vm/step-4.png b/assets/create-vm/step-4.png new file mode 100644 index 000000000..85832d260 Binary files /dev/null and b/assets/create-vm/step-4.png differ diff --git a/assets/create-vm/step-5.png b/assets/create-vm/step-5.png new file mode 100644 index 000000000..d34cb16a7 Binary files /dev/null and b/assets/create-vm/step-5.png differ diff --git a/assets/create-vm/step-6.png b/assets/create-vm/step-6.png new file mode 100644 index 000000000..92f2c2f1f Binary files /dev/null and b/assets/create-vm/step-6.png differ diff --git a/assets/create-vm/step-7.png b/assets/create-vm/step-7.png new file mode 100644 index 000000000..ad8eb9b81 Binary files /dev/null and b/assets/create-vm/step-7.png differ diff --git a/assets/create-vm/step-8.png b/assets/create-vm/step-8.png new file mode 100644 index 000000000..ce5443c76 Binary files /dev/null and b/assets/create-vm/step-8.png differ diff --git a/assets/create-vm/step-9.png b/assets/create-vm/step-9.png new file mode 100644 index 000000000..042735490 Binary files /dev/null and b/assets/create-vm/step-9.png differ diff --git a/build-cargo-dep.sh b/build-cargo-dep.sh index f3cb8e969..922dfbdf9 100755 --- a/build-cargo-dep.sh +++ b/build-cargo-dep.sh @@ -17,9 +17,18 @@ if [ -z "$ARCH" ]; then ARCH=$(uname -m) fi +DOCKER_PLATFORM="linux/${ARCH}" +if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then + DOCKER_PLATFORM="linux/arm64" +elif [ "$ARCH" = x86_64 ]; then + DOCKER_PLATFORM="linux/amd64" +fi + mkdir -p cargo-deps -alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -P start9/rust-arm-cross:aarch64' +alias 'rust-musl-builder'='docker run $USE_TTY --platform=${DOCKER_PLATFORM} --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P rust:alpine' + +PREINSTALL=${PREINSTALL:-true} -rust-arm64-builder cargo install "$1" --target-dir /home/rust/src --target=$ARCH-unknown-linux-gnu +rust-musl-builder sh -c "$PREINSTALL && cargo install $* --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl" sudo chown -R $USER cargo-deps sudo chown -R $USER ~/.cargo \ No newline at end of file diff --git a/build/.gitignore b/build/.gitignore index 357c0e49f..497f4b913 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -1,2 +1,2 @@ -lib/depends -lib/conflicts \ No newline at end of file +/lib/depends +/lib/conflicts \ No newline at end of file diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 3bab01866..000000000 --- a/build/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Building StartOS - -⚠️ The commands given assume a Debian or Ubuntu-based environment. _Building in -a VM is NOT yet supported_ ⚠️ - -## Prerequisites - -1. Install dependencies - -- Avahi - - `sudo apt install -y avahi-daemon` - - Installed by default on most Debian systems - https://avahi.org -- Build Essentials (needed to run `make`) - - `sudo apt install -y build-essential` -- Docker - - `curl -fsSL https://get.docker.com | sh` - - https://docs.docker.com/get-docker - - Add your user to the docker group: `sudo usermod -a -G docker $USER` - - Reload user environment `exec sudo su -l $USER` -- Prepare Docker environment - - Setup buildx (https://docs.docker.com/buildx/working-with-buildx/) - - Create a builder: `docker buildx create --use` - - Add multi-arch build ability: - `docker run --rm --privileged linuxkit/binfmt:v0.8` -- Node Version 12+ - - snap: `sudo snap install node` - - [nvm](https://github.com/nvm-sh/nvm#installing-and-updating): - `nvm install --lts` - - https://nodejs.org/en/docs -- NPM Version 7+ - - apt: `sudo apt install -y npm` - - [nvm](https://github.com/nvm-sh/nvm#installing-and-updating): - `nvm install --lts` - - https://docs.npmjs.com/downloading-and-installing-node-js-and-npm -- jq - - `sudo apt install -y jq` - - https://stedolan.github.io/jq -- yq - - snap: `sudo snap install yq` - - binaries: https://github.com/mikefarah/yq/releases/ - - https://mikefarah.gitbook.io/yq - -2. Clone the latest repo with required submodules - > :information_source: You chan check latest available version - > [here](https://github.com/Start9Labs/start-os/releases) - ``` - git clone --recursive https://github.com/Start9Labs/start-os.git --branch latest - ``` - -## Build Raspberry Pi Image - -``` -cd start-os -make embassyos-raspi.img ARCH=aarch64 -``` - -## Flash - -Flash the resulting `embassyos-raspi.img` to your SD Card - -We recommend [Balena Etcher](https://www.balena.io/etcher/) - -## Setup - -Visit http://start.local from any web browser - We recommend -[Firefox](https://www.mozilla.org/firefox/browsers) - -Enter your product key. This is generated during the build process and can be -found in `product_key.txt`, located in the root directory. - -## Troubleshooting - -1. I just flashed my SD card, fired up StartOS, bootup sounds and all, but my - browser is saying "Unable to connect" with start.local. - -- Try doing a hard refresh on your browser, or opening the url in a - private/incognito window. If you've ran an instance of StartOS before, - sometimes you can have a stale cache that will block you from navigating to - the page. - -2. Flashing the image isn't working with balenaEtcher. I'm getting - `Cannot read property 'message' of null` when I try. - -- The latest versions of Balena may not flash properly. This version here: - https://github.com/balena-io/etcher/releases/tag/v1.5.122 should work - properly. - -3. Startup isn't working properly and I'm curious as to why. How can I view logs - regarding startup for debugging? - -- Find the IP of your device -- Run `nc 8080` and it will print the logs - -4. I need to ssh into my server to fix something, but I cannot get to the - console to add ssh keys normally. - -- During the Build step, instead of running just - `make embassyos-raspi.img ARCH=aarch64` run - `ENVIRONMENT=dev make embassyos-raspi.img ARCH=aarch64`. Flash like normal, - and insert into your server. Boot up StartOS, then on another computer on - the same network, ssh into the the server with the username `start9` password - `embassy`. - -4. I need to reset my password, how can I do that? - -- You will need to reflash your device. Select "Use Existing Drive" once you are - in setup, and it will prompt you to set a new password. diff --git a/build/RELEASE.md b/build/RELEASE.md deleted file mode 100644 index bdbecc00e..000000000 --- a/build/RELEASE.md +++ /dev/null @@ -1,76 +0,0 @@ -# Release Process - -## `embassyos_0.3.x-1_amd64.deb` - -- Description: debian package for x86_64 - intended to be installed on pureos -- Destination: GitHub Release Tag -- Requires: N/A -- Build steps: - - Clone `https://github.com/Start9Labs/embassy-os-deb` at `master` - - Run `make TAG=master` from that folder -- Artifact: `./embassyos_0.3.x-1_amd64.deb` - -## `eos---_amd64.iso` - -- Description: live usb image for x86_64 -- Destination: GitHub Release Tag -- Requires: `embassyos_0.3.x-1_amd64.deb` -- Build steps: - - Clone `https://github.com/Start9Labs/eos-image-recipes` at `master` - - Copy `embassyos_0.3.x-1_amd64.deb` to - `overlays/vendor/root/embassyos_0.3.x-1_amd64.deb` - - Run `./run-local-build.sh byzantium` from that folder -- Artifact: `./results/eos---_amd64.iso` - -## `eos.x86_64.squashfs` - -- Description: compressed embassyOS x86_64 filesystem image -- Destination: GitHub Release Tag, Registry @ - `resources/eos//eos.x86_64.squashfs` -- Requires: `eos---_amd64.iso` -- Build steps: - - From `https://github.com/Start9Labs/eos-image-recipes` at `master` - - `./extract-squashfs.sh results/eos---_amd64.iso` -- Artifact: `./results/eos.x86_64.squashfs` - -## `eos.raspberrypi.squashfs` - -- Description: compressed embassyOS raspberrypi filesystem image -- Destination: GitHub Release Tag, Registry @ - `resources/eos//eos.raspberrypi.squashfs` -- Requires: N/A -- Build steps: - - Clone `https://github.com/Start9Labs/embassy-os` at `master` - - `make embassyos-raspi.img` - - flash `embassyos-raspi.img` to raspberry pi - - boot raspberry pi with ethernet - - wait for chime - - you can watch logs using `nc 8080` - - unplug raspberry pi, put sd card back in build machine - - `./build/raspberry-pi/rip-image.sh` -- Artifact: `./eos.raspberrypi.squashfs` - -## `lite-upgrade.img` - -- Description: update image for users coming from 0.3.2.1 and before -- Destination: Registry @ `resources/eos//eos.img` -- Requires: `eos.raspberrypi.squashfs` -- Build steps: - - From `https://github.com/Start9Labs/embassy-os` at `master` - - `make lite-upgrade.img` -- Artifact `./lite-upgrade.img` - -## `eos---_raspberrypi.tar.gz` - -- Description: pre-initialized raspberrypi image -- Destination: GitHub Release Tag (as tar.gz) -- Requires: `eos.raspberrypi.squashfs` -- Build steps: - - From `https://github.com/Start9Labs/embassy-os` at `master` - - `make eos_raspberrypi.img` - - `tar --format=posix -cS -f- eos---_raspberrypi.img | gzip > eos---_raspberrypi.tar.gz` -- Artifact `./eos---_raspberrypi.tar.gz` - -## `embassy-sdk` - -- Build and deploy to all registries \ No newline at end of file diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index a712d4a52..4c2dbc557 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -1,5 +1,6 @@ avahi-daemon avahi-utils +b3sum bash-completion beep bmon @@ -8,24 +9,28 @@ ca-certificates cifs-utils cryptsetup curl +dnsutils dmidecode +dnsutils dosfstools e2fsprogs ecryptfs-utils exfatprogs flashrom +fuse3 grub-common htop httpdirfs iotop +iptables iw jq -libavahi-client3 libyajl2 linux-cpupower lm-sensors lshw lvm2 +lxc magic-wormhole man-db ncdu @@ -41,8 +46,10 @@ qemu-guest-agent rsync samba-common-bin smartmontools +socat sqlite3 squashfs-tools +squashfs-tools-ng sudo systemd systemd-resolved @@ -51,4 +58,5 @@ systemd-timesyncd tor util-linux vim +wireguard-tools wireless-tools diff --git a/build/dpkg-deps/docker.depends b/build/dpkg-deps/docker.depends deleted file mode 100644 index dd78be8a1..000000000 --- a/build/dpkg-deps/docker.depends +++ /dev/null @@ -1,5 +0,0 @@ -+ containerd.io -+ docker-ce -+ docker-ce-cli -+ docker-compose-plugin -- podman \ No newline at end of file diff --git a/build/lib/firmware.json b/build/lib/firmware.json index 79e46d050..e3268ddf8 100644 --- a/build/lib/firmware.json +++ b/build/lib/firmware.json @@ -1,13 +1,13 @@ [ { - "id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3", + "id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29", "platform": ["x86_64"], "system-product-name": "librem_mini_v2", "bios-version": { "semver-prefix": "PureBoot-Release-", - "semver-range": "<28.3" + "semver-range": "<29" }, - "url": "https://source.puri.sm/firmware/releases/-/raw/98418b5b8e9edc2bd1243ad7052a062f79e2b88e/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3.rom.gz", - "shasum": "5019bcf53f7493c7aa74f8ef680d18b5fc26ec156c705a841433aaa2fdef8f35" + "url": "https://source.puri.sm/firmware/releases/-/raw/75631ad6dcf7e6ee73e06a517ac7dc4e017518b7/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29.rom.gz", + "shasum": "96ec04f21b1cfe8e28d9a2418f1ff533efe21f9bbbbf16e162f7c814761b068b" } ] diff --git a/build/lib/scripts/add-apt-sources b/build/lib/scripts/add-apt-sources index 638d8dad6..9d4f54a28 100755 --- a/build/lib/scripts/add-apt-sources +++ b/build/lib/scripts/add-apt-sources @@ -4,6 +4,3 @@ set -e curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor -o- > /usr/share/keyrings/tor-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org bullseye main" > /etc/apt/sources.list.d/tor.list - -curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o- > /usr/share/keyrings/docker-archive-keyring.gpg -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list diff --git a/build/lib/scripts/chroot-and-upgrade b/build/lib/scripts/chroot-and-upgrade index f95e49924..8c3da37b4 100755 --- a/build/lib/scripts/chroot-and-upgrade +++ b/build/lib/scripts/chroot-and-upgrade @@ -1,46 +1,107 @@ #!/bin/bash +SOURCE_DIR="$(dirname "${BASH_SOURCE[0]}")" + if [ "$UID" -ne 0 ]; then >&2 echo 'Must be run as root' exit 1 fi +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + --no-sync) + NO_SYNC=1 + shift + ;; + --create) + ONLY_CREATE=1 + shift + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + if [ -z "$NO_SYNC" ]; then echo 'Syncing...' - rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next + umount -R /media/startos/next 2> /dev/null + umount -R /media/startos/upper 2> /dev/null + rm -rf /media/startos/upper /media/startos/next + mkdir /media/startos/upper + mount -t tmpfs tmpfs /media/startos/upper + mkdir -p /media/startos/upper/data /media/startos/upper/work /media/startos/next + mount -t overlay \ + -olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \ + overlay /media/startos/next + mkdir -p /media/startos/next/media/startos/root + mount --bind /media/startos/root /media/startos/next/media/startos/root +fi + +if [ -n "$ONLY_CREATE" ]; then + exit 0 fi -mkdir -p /media/embassy/next/run -mkdir -p /media/embassy/next/dev -mkdir -p /media/embassy/next/sys -mkdir -p /media/embassy/next/proc -mkdir -p /media/embassy/next/boot -mount --bind /run /media/embassy/next/run -mount --bind /dev /media/embassy/next/dev -mount --bind /sys /media/embassy/next/sys -mount --bind /proc /media/embassy/next/proc -mount --bind /boot /media/embassy/next/boot +mkdir -p /media/startos/next/run +mkdir -p /media/startos/next/dev +mkdir -p /media/startos/next/sys +mkdir -p /media/startos/next/proc +mkdir -p /media/startos/next/boot +mount --bind /run /media/startos/next/run +mount --bind /tmp /media/startos/next/tmp +mount --bind /dev /media/startos/next/dev +mount --bind /sys /media/startos/next/sys +mount --bind /proc /media/startos/next/proc +mount --bind /boot /media/startos/next/boot if [ -z "$*" ]; then - chroot /media/embassy/next + chroot /media/startos/next CHROOT_RES=$? else - chroot /media/embassy/next "$SHELL" -c "$*" + chroot /media/startos/next "$SHELL" -c "$*" CHROOT_RES=$? fi -umount /media/embassy/next/run -umount /media/embassy/next/dev -umount /media/embassy/next/sys -umount /media/embassy/next/proc -umount /media/embassy/next/boot +umount /media/startos/next/run +umount /media/startos/next/tmp +umount /media/startos/next/dev +umount /media/startos/next/sys +umount /media/startos/next/proc +umount /media/startos/next/boot +umount /media/startos/next/media/startos/root if [ "$CHROOT_RES" -eq 0 ]; then + + if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then + ${SOURCE_DIR}/prune-images $(du -s --bytes /media/startos/next | awk '{print $1}') + fi + echo 'Upgrading...' - touch /media/embassy/config/upgrade + if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then + umount -R /media/startos/next + umount -R /media/startos/upper + rm -rf /media/startos/upper /media/startos/next + exit 1 + fi + hash=$(b3sum /media/startos/images/next.squashfs | head -c 32) + mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs + ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs sync reboot -fi \ No newline at end of file +fi + +umount -R /media/startos/next +umount -R /media/startos/upper +rm -rf /media/startos/upper /media/startos/next \ No newline at end of file diff --git a/build/lib/scripts/dhclient-exit-hook b/build/lib/scripts/dhclient-exit-hook deleted file mode 100755 index 8c4a97746..000000000 --- a/build/lib/scripts/dhclient-exit-hook +++ /dev/null @@ -1 +0,0 @@ -start-cli net dhcp update $interface \ No newline at end of file diff --git a/build/lib/scripts/embassy-initramfs-module b/build/lib/scripts/embassy-initramfs-module deleted file mode 100755 index 2a2f08a07..000000000 --- a/build/lib/scripts/embassy-initramfs-module +++ /dev/null @@ -1,98 +0,0 @@ -# Local filesystem mounting -*- shell-script -*- - -# -# This script overrides local_mount_root() in /scripts/local -# and mounts root as a read-only filesystem with a temporary (rw) -# overlay filesystem. -# - -. /scripts/local - -local_mount_root() -{ - echo 'using embassy initramfs module' - - local_top - local_device_setup "${ROOT}" "root file system" - ROOT="${DEV}" - - # Get the root filesystem type if not set - if [ -z "${ROOTFSTYPE}" ]; then - FSTYPE=$(get_fstype "${ROOT}") - else - FSTYPE=${ROOTFSTYPE} - fi - - local_premount - - # CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE - # N.B. this code still lacks error checking - - modprobe ${FSTYPE} - checkfs ${ROOT} root "${FSTYPE}" - - ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')" - - if [ "${FSTYPE}" != "unknown" ]; then - mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} ${rootmnt} - else - mount ${ROOTFLAGS} ${ROOT} ${rootmnt} - fi - - echo 'mounting embassyfs' - - mkdir /embassyfs - - mount --move ${rootmnt} /embassyfs - - if ! [ -d /embassyfs/current ] && [ -d /embassyfs/prev ]; then - mv /embassyfs/prev /embassyfs/current - fi - - if ! [ -d /embassyfs/current ]; then - mkdir /embassyfs/current - for FILE in $(ls /embassyfs); do - if [ "$FILE" != current ]; then - mv /embassyfs/$FILE /embassyfs/current/ - fi - done - fi - - mkdir -p /embassyfs/config - - if [ -f /embassyfs/config/upgrade ] && [ -d /embassyfs/next ]; then - mv /embassyfs/current /embassyfs/prev - mv /embassyfs/next /embassyfs/current - rm /embassyfs/config/upgrade - fi - - if ! [ -d /embassyfs/next ]; then - if [ -d /embassyfs/prev ]; then - mv /embassyfs/prev /embassyfs/next - else - mkdir /embassyfs/next - fi - fi - - mkdir /lower /upper - - mount -r --bind /embassyfs/current /lower - - modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko" - - # Mount a tmpfs for the overlay in /upper - mount -t tmpfs tmpfs /upper - mkdir /upper/data /upper/work - - # Mount the final overlay-root in $rootmnt - mount -t overlay \ - -olowerdir=/lower,upperdir=/upper/data,workdir=/upper/work \ - overlay ${rootmnt} - - mkdir -p ${rootmnt}/media/embassy/config - mount --bind /embassyfs/config ${rootmnt}/media/embassy/config - mkdir -p ${rootmnt}/media/embassy/next - mount --bind /embassyfs/next ${rootmnt}/media/embassy/next - mkdir -p ${rootmnt}/media/embassy/embassyfs - mount -r --bind /embassyfs ${rootmnt}/media/embassy/embassyfs -} \ No newline at end of file diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index ad7cd4bf3..548542d23 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -4,7 +4,7 @@ set -e # install dependencies /usr/bin/apt update -/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools +/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools p11-kit-modules #Change a default preference set by stock debian firefox-esr sed -i 's|^pref("extensions.update.enabled", true);$|pref("extensions.update.enabled", false);|' /etc/firefox-esr/firefox-esr.js @@ -14,14 +14,8 @@ if ! id kiosk; then useradd -s /bin/bash --create-home kiosk fi -# create kiosk script -cat > /home/kiosk/kiosk.sh << 'EOF' -#!/bin/sh -PROFILE=$(mktemp -d) -if [ -f /usr/local/share/ca-certificates/startos-root-ca.crt ]; then - certutil -A -n "StartOS Local Root CA" -t "TCu,Cuw,Tuw" -i /usr/local/share/ca-certificates/startos-root-ca.crt -d $PROFILE -fi -cat >> $PROFILE/prefs.js << EOT +mkdir /home/kiosk/fx-profile +cat >> /home/kiosk/fx-profile/prefs.js << EOF user_pref("app.normandy.api_url", ""); user_pref("app.normandy.enabled", false); user_pref("app.shield.optoutstudies.enabled", false); @@ -87,7 +81,13 @@ user_pref("toolkit.telemetry.shutdownPingSender.enabled", false); user_pref("toolkit.telemetry.unified", false); user_pref("toolkit.telemetry.updatePing.enabled", false); user_pref("toolkit.telemetry.cachedClientID", ""); -EOT +EOF + +ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so + +# create kiosk script +cat > /home/kiosk/kiosk.sh << 'EOF' +#!/bin/sh while ! curl "http://localhost" > /dev/null; do sleep 1 done @@ -101,8 +101,9 @@ done killall firefox-esr ) & matchbox-window-manager -use_titlebar no & -firefox-esr http://localhost --profile $PROFILE -rm -rf $PROFILE +cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp +firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp +rm -rf /home/kiosk/fx-profile-tmp EOF chmod +x /home/kiosk/kiosk.sh @@ -116,6 +117,8 @@ fi EOF fi +chown -R kiosk:kiosk /home/kiosk + # enable autologin mkdir -p /etc/systemd/system/getty@tty1.service.d cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << 'EOF' diff --git a/build/lib/scripts/gather-debug-info b/build/lib/scripts/gather-debug-info new file mode 100755 index 000000000..a47ca60bd --- /dev/null +++ b/build/lib/scripts/gather-debug-info @@ -0,0 +1,105 @@ +#!/bin/bash + +# Define the output file +OUTPUT_FILE="system_debug_info.txt" + +# Check if the script is run as root, if not, restart with sudo +if [ "$(id -u)" -ne 0 ]; then + exec sudo bash "$0" "$@" +fi + +# Create or clear the output file and add a header +echo "===================================================================" > "$OUTPUT_FILE" +echo " StartOS System Debug Information " >> "$OUTPUT_FILE" +echo "===================================================================" >> "$OUTPUT_FILE" +echo "Generated on: $(date)" >> "$OUTPUT_FILE" +echo "" >> "$OUTPUT_FILE" + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to run a command if it exists and append its output to the file with headers +run_command() { + local CMD="$1" + local DESC="$2" + local CMD_NAME="${CMD%% *}" # Extract the command name (first word) + + if command_exists "$CMD_NAME"; then + echo "===================================================================" >> "$OUTPUT_FILE" + echo "COMMAND: $CMD" >> "$OUTPUT_FILE" + echo "DESCRIPTION: $DESC" >> "$OUTPUT_FILE" + echo "===================================================================" >> "$OUTPUT_FILE" + echo "" >> "$OUTPUT_FILE" + eval "$CMD" >> "$OUTPUT_FILE" 2>&1 + echo "" >> "$OUTPUT_FILE" + else + echo "===================================================================" >> "$OUTPUT_FILE" + echo "COMMAND: $CMD" >> "$OUTPUT_FILE" + echo "DESCRIPTION: $DESC" >> "$OUTPUT_FILE" + echo "===================================================================" >> "$OUTPUT_FILE" + echo "SKIPPED: Command not found" >> "$OUTPUT_FILE" + echo "" >> "$OUTPUT_FILE" + fi +} + +# Collecting basic system information +run_command "start-cli --version; start-cli git-info" "StartOS CLI version and Git information" +run_command "hostname" "Hostname of the system" +run_command "uname -a" "Kernel version and system architecture" + +# Services Info +run_command "start-cli lxc stats" "All Running Services" + +# Collecting CPU information +run_command "lscpu" "CPU architecture information" +run_command "cat /proc/cpuinfo" "Detailed CPU information" + +# Collecting memory information +run_command "free -h" "Available and used memory" +run_command "cat /proc/meminfo" "Detailed memory information" + +# Collecting storage information +run_command "lsblk" "List of block devices" +run_command "df -h" "Disk space usage" +run_command "fdisk -l" "Detailed disk partition information" + +# Collecting network information +run_command "ip a" "Network interfaces and IP addresses" +run_command "ip route" "Routing table" +run_command "netstat -i" "Network interface statistics" + +# Collecting RAID information (if applicable) +run_command "cat /proc/mdstat" "List of RAID devices (if applicable)" + +# Collecting virtualization information +run_command "egrep -c '(vmx|svm)' /proc/cpuinfo" "Check if CPU supports virtualization" +run_command "systemd-detect-virt" "Check if the system is running inside a virtual machine" + +# Final message +echo "===================================================================" >> "$OUTPUT_FILE" +echo " End of StartOS System Debug Information " >> "$OUTPUT_FILE" +echo "===================================================================" >> "$OUTPUT_FILE" + +# Prompt user to send the log file to a Start9 Technician +echo "System debug information has been collected in $OUTPUT_FILE." +echo "" +echo "Would you like to send this log file to a Start9 Technician? (yes/no)" +read SEND_LOG + +if [[ "$SEND_LOG" == "yes" || "$SEND_LOG" == "y" ]]; then + if command -v wormhole >/dev/null 2>&1; then + echo "" + echo "===================================================================" + echo " Running wormhole to send the file. Please follow the " + echo " instructions and provide the code to the Start9 support team. " + echo "===================================================================" + wormhole send "$OUTPUT_FILE" + echo "===================================================================" + else + echo "Error: wormhole command not found." + fi +else + echo "Log file not sent. You can manually share $OUTPUT_FILE with the Start9 support team if needed." +fi \ No newline at end of file diff --git a/build/lib/scripts/grub-probe-eos b/build/lib/scripts/grub-probe-eos index ed37eefaa..aa2e1cacc 100755 --- a/build/lib/scripts/grub-probe-eos +++ b/build/lib/scripts/grub-probe-eos @@ -3,8 +3,8 @@ ARGS= for ARG in $@; do - if [ -d "/media/embassy/embassyfs" ] && [ "$ARG" = "/" ]; then - ARG=/media/embassy/embassyfs + if [ -d "/media/startos/root" ] && [ "$ARG" = "/" ]; then + ARG=/media/startos/root fi ARGS="$ARGS $ARG" done diff --git a/build/lib/scripts/prune-images b/build/lib/scripts/prune-images new file mode 100755 index 000000000..1203d2377 --- /dev/null +++ b/build/lib/scripts/prune-images @@ -0,0 +1,50 @@ +#!/bin/bash + +if [ "$UID" -ne 0 ]; then + >&2 echo 'Must be run as root' + exit 1 +fi + +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + +needed=$1 + +if [ -z "$needed" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then + echo 'Pruning...' + current="$(readlink -f /media/startos/config/current.rootfs)" + while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do + to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)" + if [ -e "$to_prune" ]; then + echo " Pruning $to_prune" + rm -rf "$to_prune" + sync + else + >&2 echo "Not enough space and nothing to prune!" + exit 1 + fi + done + echo 'done.' +else + >&2 echo 'No current.rootfs, not safe to prune' + exit 1 +fi \ No newline at end of file diff --git a/build/lib/scripts/startos-initramfs-module b/build/lib/scripts/startos-initramfs-module new file mode 100755 index 000000000..e13c887e2 --- /dev/null +++ b/build/lib/scripts/startos-initramfs-module @@ -0,0 +1,114 @@ +# Local filesystem mounting -*- shell-script -*- + +# +# This script overrides local_mount_root() in /scripts/local +# and mounts root as a read-only filesystem with a temporary (rw) +# overlay filesystem. +# + +. /scripts/local + +local_mount_root() +{ + echo 'using startos initramfs module' + + local_top + local_device_setup "${ROOT}" "root file system" + ROOT="${DEV}" + + # Get the root filesystem type if not set + if [ -z "${ROOTFSTYPE}" ]; then + FSTYPE=$(get_fstype "${ROOT}") + else + FSTYPE=${ROOTFSTYPE} + fi + + local_premount + + # CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE + # N.B. this code still lacks error checking + + modprobe ${FSTYPE} + checkfs ${ROOT} root "${FSTYPE}" + + echo 'mounting startos' + mkdir /startos + + ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')" + + if [ "${FSTYPE}" != "unknown" ]; then + mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} /startos + else + mount ${ROOTFLAGS} ${ROOT} /startos + fi + + if [ -d /startos/images ]; then + if [ -h /startos/config/current.rootfs ] && [ -e /startos/config/current.rootfs ]; then + image=$(readlink -f /startos/config/current.rootfs) + else + image="$(ls -t1 /startos/images/*.rootfs | head -n1)" + fi + if ! [ -f "$image" ]; then + >&2 echo "image $image not available to boot" + exit 1 + fi + else + if [ -f /startos/config/upgrade ] && [ -d /startos/next ]; then + oldroot=/startos/next + elif [ -d /startos/current ]; then + oldroot=/startos/current + elif [ -d /startos/prev ]; then + oldroot=/startos/prev + else + >&2 echo no StartOS filesystem found + exit 1 + fi + + mkdir -p /startos/config/overlay/etc + mv $oldroot/etc/fstab /startos/config/overlay/etc/fstab + mv $oldroot/etc/machine-id /startos/config/overlay/etc/machine-id + mv $oldroot/etc/ssh /startos/config/overlay/etc/ssh + + mkdir -p /startos/images + mv $oldroot /startos/images/legacy.rootfs + + rm -rf /startos/next /startos/current /startos/prev + + ln -rsf /startos/images/old.squashfs /startos/config/current.rootfs + image=$(readlink -f /startos/config/current.rootfs) + fi + + mkdir /lower /upper + + if [ -d "$image" ]; then + mount -r --bind $image /lower + elif [ -f "$image" ]; then + modprobe squashfs + mount -r $image /lower + else + >&2 echo "not a regular file or directory: $image" + exit 1 + fi + + modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko" + + # Mount a tmpfs for the overlay in /upper + mount -t tmpfs tmpfs /upper + mkdir /upper/data /upper/work + + mkdir -p /startos/config/overlay + + # Mount the final overlay-root in $rootmnt + mount -t overlay \ + -olowerdir=/startos/config/overlay:/lower,upperdir=/upper/data,workdir=/upper/work \ + overlay ${rootmnt} + + mkdir -p ${rootmnt}/media/startos/config + mount --bind /startos/config ${rootmnt}/media/startos/config + mkdir -p ${rootmnt}/media/startos/images + mount --bind /startos/images ${rootmnt}/media/startos/images + mkdir -p ${rootmnt}/media/startos/root + mount -r --bind /startos ${rootmnt}/media/startos/root + mkdir -p ${rootmnt}/media/startos/current + mount -r --bind /lower ${rootmnt}/media/startos/current +} \ No newline at end of file diff --git a/build/lib/scripts/tor-check.sh b/build/lib/scripts/tor-check similarity index 100% rename from build/lib/scripts/tor-check.sh rename to build/lib/scripts/tor-check diff --git a/build/lib/scripts/wg-vps-setup b/build/lib/scripts/wg-vps-setup new file mode 100755 index 000000000..6c630bb46 --- /dev/null +++ b/build/lib/scripts/wg-vps-setup @@ -0,0 +1,367 @@ +#!/bin/bash + +# Colors for better output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[1;34m' +YELLOW='\033[1;33m' +NC='\033[0;37m' # No Color + +# --- Constants --- +readonly WIREGUARD_INSTALL_URL="https://raw.githubusercontent.com/start9labs/wg-vps-setup/master/wireguard-install.sh" +readonly SSH_KEY_DIR="/home/start9/.ssh" +readonly SSH_KEY_NAME="id_ed25519" +readonly SSH_PRIVATE_KEY="$SSH_KEY_DIR/$SSH_KEY_NAME" +readonly SSH_PUBLIC_KEY="$SSH_PRIVATE_KEY.pub" + +# Store original arguments +SCRIPT_ARGS=("$@") + +# --- Functions --- + +# Function to ensure script runs with root privileges by auto-elevating if needed +check_root() { + if [[ "$EUID" -ne 0 ]]; then + exec sudo "$0" "${SCRIPT_ARGS[@]}" + fi + sudo chown -R start9:startos "$SSH_KEY_DIR" +} + +# Function to print banner +print_banner() { + echo -e "${BLUE}" + echo "================================================" + echo -e " ${NC}StartOS WireGuard VPS Setup Tool${BLUE} " + echo "================================================" + echo -e "${NC}" +} + +# Function to print usage +print_usage() { + echo -e "Usage: $0 [-h] [-i IP] [-u USERNAME] [-p PORT] [-k SSH_KEY]" + echo "Options:" + echo " -h Show this help message" + echo " -i VPS IP address" + echo " -u SSH username (default: root)" + echo " -p SSH port (default: 22)" + echo " -k Path to the custom SSH private key (optional)" + echo " If no key is provided, the default key '$SSH_PRIVATE_KEY' will be used." +} + +# Function to display end message +display_end_message() { + echo -e "\n${BLUE}------------------------------------------------------------------${NC}" + echo -e "${NC}WireGuard server setup complete!" + echo -e "${BLUE}------------------------------------------------------------------${NC}" + echo -e "\n${YELLOW}To expose your services to the Clearnet, use the following commands on your StartOS system (replace placeholders):${NC}" + echo -e "\n ${YELLOW}1. Initialize ACME (This only needs to be done once):${NC}" + echo " start-cli net acme init --provider=letsencrypt --contact=mailto:your-email@example.com" + echo -e "\n ${YELLOW}2. Expose 'hello-world' on port 80 through VPS:${NC}" + echo " start-cli package host hello-world binding ui-multi set-public 80" + echo -e "\n ${YELLOW}3. Add a domain to your 'hello-world' service:${NC}" + echo " start-cli package host hello-world address ui-multi domain add your-domain.example.com --acme=letsencrypt" + echo -e "\n ${YELLOW}Replace '${NC}your-email@example.com${YELLOW}' with your actual email address, '${NC}your-domain.example.com${YELLOW}' with your actual domain and '${NC}hello-world${YELLOW}' with your actual service id.${NC}" + echo -e "${BLUE}------------------------------------------------------------------${NC}" +} + +# Function to validate IP address +validate_ip() { + local ip=$1 + if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + return 0 + else + return 1 + fi +} + +# Function for configuring SSH key authentication on remote server +configure_ssh_key_auth() { + echo -e "${BLUE}Configuring SSH key authentication on remote server...${NC}" + + ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" ' + # Check if PubkeyAuthentication is commented out + if grep -q "^#PubkeyAuthentication" /etc/ssh/sshd_config; then + sed -i "s/^#PubkeyAuthentication.*/PubkeyAuthentication yes/" /etc/ssh/sshd_config + # Check if PubkeyAuthentication exists but is not enabled + elif grep -q "^PubkeyAuthentication" /etc/ssh/sshd_config; then + sed -i "s/^PubkeyAuthentication.*/PubkeyAuthentication yes/" /etc/ssh/sshd_config + # Add PubkeyAuthentication if it doesnt exist + else + echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config + fi + + # Configure AuthorizedKeysFile if needed + if grep -q "^#AuthorizedKeysFile" /etc/ssh/sshd_config; then + sed -i "s/^#AuthorizedKeysFile.*/AuthorizedKeysFile .ssh\/authorized_keys .ssh\/authorized_keys2/" /etc/ssh/sshd_config + elif ! grep -q "^AuthorizedKeysFile" /etc/ssh/sshd_config; then + echo "AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2" >> /etc/ssh/sshd_config + fi + + # Reload SSH service + systemctl reload sshd + ' +} + +# Function to handle StartOS connection (download only) +handle_startos_connection() { + echo -e "${BLUE}Fetching the WireGuard configuration file...${NC}" + + # Fetch the client configuration file + config_file=$(ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" 'ls -t ~/*.conf 2>/dev/null | head -n 1') + if [ -z "$config_file" ]; then + echo -e "${RED}Error: No WireGuard configuration file found on the remote server.${NC}" + return 1 # Exit with error + fi + CONFIG_NAME=$(basename "$config_file") + + # Download the configuration file + if ! scp -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -P "$SSH_PORT" "$SSH_USER@$VPS_IP":~/"$CONFIG_NAME" ./; then + echo -e "${RED}Error: Failed to download the WireGuard configuration file.${NC}" + return 1 # Exit with error + fi + echo -e "${GREEN}WireGuard configuration file '$CONFIG_NAME' downloaded successfully.${NC}" + return 0 +} + +# Function to import WireGuard configuration +import_wireguard_config() { + local config_name="$1" + if [ -z "$config_name" ]; then + echo -e "${RED}Error: Configuration file name is missing.${NC}" + return 1 + fi + + local connection_name=$(basename "$config_name" .conf) #Extract base name without extension + + # Check if the connection with same name already exists + if nmcli connection show --active | grep -q "^${connection_name}\s"; then + read -r -p "A connection with the name '$connection_name' already exists. Do you want to override it? (y/N): " answer + if [[ "$answer" =~ ^[Yy]$ ]]; then + nmcli connection delete "$connection_name" + if [ $? -ne 0 ]; then + echo -e "${RED}Error: Failed to delete existing connection '$connection_name'.${NC}" + return 1 + fi + # Import if user chose to override or if connection did not exist + if ! nmcli connection import type wireguard file "$config_name"; then + echo -e "${RED}Error: Failed to import the WireGuard configuration using NetworkManager.${NC}" + rm -f "$config_name" + return 1 + fi + echo -e "${GREEN}WireGuard configuration '$config_name' has been imported to NetworkManager.${NC}" + rm -f "$config_name" + display_end_message + else + echo -e "${BLUE}Skipping import of the WireGuard configuration.${NC}" + rm -f "$config_name" + return 0 + fi + else + # Import if connection did not exist + if command -v nmcli &>/dev/null; then + if ! nmcli connection import type wireguard file "$config_name"; then + echo -e "${RED}Error: Failed to import the WireGuard configuration using NetworkManager.${NC}" + rm -f "$config_name" + return 1 + fi + echo -e "${GREEN}WireGuard configuration '$config_name' has been imported to NetworkManager.${NC}" + rm -f "$config_name" + display_end_message + else + echo -e "${YELLOW}Warning: NetworkManager 'nmcli' not found. Configuration file '$config_name' saved in current directory.${NC}" + echo -e "${YELLOW}Import the configuration to your StartOS manually by going to NetworkManager or using wg-quick up command${NC}" + fi + fi + return 0 +} + +# Function to download the install script +download_install_script() { + echo -e "${BLUE}Downloading latest WireGuard install script...${NC}" + # Download the script + if ! curl -sSf "$WIREGUARD_INSTALL_URL" -o wireguard-install.sh; then + echo -e "${RED}Failed to download WireGuard installation script.${NC}" + return 1 + fi + chmod +x wireguard-install.sh + if [ $? -ne 0 ]; then + echo -e "${RED}Failed to chmod +x wireguard install script.${NC}" + return 1 + fi + echo -e "${GREEN}WireGuard install script downloaded successfully!${NC}" + return 0 +} + +# Function to install WireGuard +install_wireguard() { + echo -e "\n${BLUE}Installing WireGuard...${NC}" + + # Check if install script exist + if [ ! -f "wireguard-install.sh" ]; then + echo -e "${RED}WireGuard install script is missing. Did it failed to download?${NC}" + return 1 + fi + + # Run the remote install script and let it complete + if ! ssh -o ConnectTimeout=60 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" -t "$SSH_USER@$VPS_IP" "bash -c 'export TERM=xterm-256color; export STARTOS_HOSTNAME=$(hostname); bash ~/wireguard-install.sh'"; then + echo -e "${RED}WireGuard installation failed on remote server.${NC}" + return 1 + fi + + # Test if wireguard installed + if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" "test -f /etc/wireguard/wg0.conf"; then + echo -e "\n${RED}WireGuard installation failed because /etc/wireguard/wg0.conf is missing, which means the script removed it.${NC}" + return 1 + fi + + echo -e "\n${GREEN}WireGuard installation completed successfully!${NC}" + return 0 +} + +# --- Main Script --- +# Initialize variables +VPS_IP="" +SSH_USER="root" +SSH_PORT="22" +CUSTOM_SSH_KEY="" +CONFIG_NAME="" + +# Check if the script is run as root before anything else +check_root + +# Print banner +print_banner + +# Parse command line arguments +while getopts "hi:u:p:k:" opt; do + case $opt in + h) + print_usage + exit 0 + ;; + i) + VPS_IP=$OPTARG + ;; + u) + SSH_USER=$OPTARG + ;; + p) + SSH_PORT=$OPTARG + ;; + k) + CUSTOM_SSH_KEY=$OPTARG + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + print_usage + exit 1 + ;; + esac +done + +# Check if custom SSH key is passed and update the private key variable +if [ -n "$CUSTOM_SSH_KEY" ]; then + if [ ! -f "$CUSTOM_SSH_KEY" ]; then + echo -e "${RED}Custom SSH key '$CUSTOM_SSH_KEY' not found.${NC}" + exit 1 + fi + SSH_PRIVATE_KEY="$CUSTOM_SSH_KEY" + SSH_PUBLIC_KEY="$CUSTOM_SSH_KEY.pub" +else + # Use default StartOS SSH key + if [ ! -f "$SSH_PRIVATE_KEY" ]; then + echo -e "${RED}No SSH key found at default location '$SSH_PRIVATE_KEY'. Please ensure StartOS SSH keys are properly configured.${NC}" + exit 1 + fi +fi + +if [ ! -f "$SSH_PUBLIC_KEY" ]; then + echo -e "${RED}Public key '$SSH_PUBLIC_KEY' not found. Please ensure both private and public keys exist.${NC}" + exit 1 +fi + +# If VPS_IP is not provided via command line, ask for it +if [ -z "$VPS_IP" ]; then + while true; do + echo -n "Please enter your VPS IP address: " + read VPS_IP + if validate_ip "$VPS_IP"; then + break + else + echo -e "${RED}Invalid IP address format. Please try again.${NC}" + fi + done +fi + +# Confirm SSH connection details +echo -e "\n${GREEN}Connection details:${NC}" +echo "VPS IP: $VPS_IP" +echo "SSH User: $SSH_USER" +echo "SSH Port: $SSH_PORT" + +echo -e "\n${GREEN}Proceeding with SSH key-based authentication...${NC}\n" + +# Copy SSH public key to the remote server +if ! ssh-copy-id -i "$SSH_PUBLIC_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP"; then + echo -e "${RED}Failed to copy SSH key to the remote server. Please ensure you have correct credentials.${NC}" + exit 1 +fi + +echo -e "${GREEN}SSH key-based authentication configured successfully!${NC}" + +# Test SSH connection using key-based authentication +echo -e "\nTesting SSH connection with key-based authentication..." +if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" 'grep -q "^PubkeyAuthentication yes" /etc/ssh/sshd_config'; then + echo -e "\n${RED}SSH key-based authentication is not enabled on your VPS.${NC}" + echo -e "\n${YELLOW}Would you like this script to automatically enable SSH key authentication? (y/N):${NC} " + read -r answer + + if [[ "$answer" =~ ^[Yy]$ ]]; then + configure_ssh_key_auth + else + echo -e "\n${BLUE}------------------------------------------------------------------${NC}" + echo -e "${YELLOW}To manually enable SSH key authentication:${NC}" + echo -e "\n ${YELLOW}1. Connect to your VPS and edit sshd_config:${NC}" + echo " nano /etc/ssh/sshd_config" + echo -e "\n ${YELLOW}2. Find and uncomment or add the line:${NC}" + echo " PubkeyAuthentication yes" + echo -e "\n ${YELLOW}3. Restart the SSH service:${NC}" + echo " systemctl restart sshd" + echo -e "${BLUE}------------------------------------------------------------------${NC}" + echo -e "\n${YELLOW}Please enable SSH key authentication and run this script again.${NC}" + exit 1 + fi +fi +echo -e "${GREEN}SSH connection successful with key-based authentication!${NC}" + +# Download the WireGuard install script locally +if ! download_install_script; then + echo -e "${RED}Failed to download the latest install script. Exiting...${NC}" + exit 1 +fi + +# Upload the install script to the remote server +if ! scp -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -P "$SSH_PORT" wireguard-install.sh "$SSH_USER@$VPS_IP":~/; then + echo -e "${RED}Failed to upload WireGuard install script to the remote server.${NC}" + exit 1 +fi + +# Install WireGuard on remote server using the downloaded script +if ! install_wireguard; then + echo -e "${RED}WireGuard installation failed.${NC}" + exit 1 +fi + +# Remove the local install script +rm wireguard-install.sh >/dev/null 2>&1 + +# Handle the StartOS config (download) +if ! handle_startos_connection; then + echo -e "${RED}StartOS configuration download failed!${NC}" + exit 1 +fi + +# Import the configuration +if ! import_wireguard_config "$CONFIG_NAME"; then + echo -e "${RED}StartOS configuration import failed or skipped!${NC}" +fi diff --git a/build/raspberrypi/make-image.sh b/build/raspberrypi/make-image.sh index 3b07cb3a8..ec5ea4297 100755 --- a/build/raspberrypi/make-image.sh +++ b/build/raspberrypi/make-image.sh @@ -63,7 +63,7 @@ sudo unsquashfs -f -d $TMPDIR startos.raspberrypi.squashfs REAL_GIT_HASH=$(cat $TMPDIR/usr/lib/startos/GIT_HASH.txt) REAL_VERSION=$(cat $TMPDIR/usr/lib/startos/VERSION.txt) REAL_ENVIRONMENT=$(cat $TMPDIR/usr/lib/startos/ENVIRONMENT.txt) -sudo sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt +sudo sed -i 's| boot=startos| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt sudo cp ./build/raspberrypi/fstab $TMPDIR/etc/ sudo cp ./build/raspberrypi/init_resize.sh $TMPDIR/usr/lib/startos/scripts/init_resize.sh sudo umount $TMPDIR/boot diff --git a/check-git-hash.sh b/check-git-hash.sh index 874dcc8bf..2f59b9198 100755 --- a/check-git-hash.sh +++ b/check-git-hash.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then - GIT_HASH="$(git describe --always --abbrev=40 --dirty=-modified)" + GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)" else GIT_HASH="@$(git rev-parse --abbrev-ref HEAD)" fi diff --git a/container-runtime/.gitignore b/container-runtime/.gitignore new file mode 100644 index 000000000..8aa4208b7 --- /dev/null +++ b/container-runtime/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +bundle.js +startInit.js +service/ +service.js +*.squashfs +/tmp \ No newline at end of file diff --git a/container-runtime/RPCSpec.md b/container-runtime/RPCSpec.md new file mode 100644 index 000000000..fd1014add --- /dev/null +++ b/container-runtime/RPCSpec.md @@ -0,0 +1,89 @@ +# Container RPC SERVER Specification + +## Methods + +### init + +initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`) + +called after os has mounted js and images to the container + +#### args + +`[]` + +#### response + +`null` + +### exit + +shutdown runtime + +#### args + +`[]` + +#### response + +`null` + +### start + +run main method if not already running + +#### args + +`[]` + +#### response + +`null` + +### stop + +stop main method by sending SIGTERM to child processes, and SIGKILL after timeout + +#### args + +`{ timeout: millis }` + +#### response + +`null` + +### execute + +run a specific package procedure + +#### args + +```ts +{ + procedure: JsonPath, + input: any, + timeout: millis, +} +``` + +#### response + +`any` + +### sandbox + +run a specific package procedure in sandbox mode + +#### args + +```ts +{ + procedure: JsonPath, + input: any, + timeout: millis, +} +``` + +#### response + +`any` diff --git a/container-runtime/container-runtime.service b/container-runtime/container-runtime.service new file mode 100644 index 000000000..b9d5ec5ae --- /dev/null +++ b/container-runtime/container-runtime.service @@ -0,0 +1,9 @@ +[Unit] +Description=StartOS Container Runtime + +[Service] +Type=simple +ExecStart=/usr/bin/node --experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/container-runtime/deb-install.sh b/container-runtime/deb-install.sh new file mode 100644 index 000000000..697bfd10e --- /dev/null +++ b/container-runtime/deb-install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +mkdir -p /run/systemd/resolve +echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf + +apt-get update +apt-get install -y curl rsync qemu-user-static + +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +source ~/.bashrc +nvm install 20 +ln -s $(which node) /usr/bin/node + +sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf +sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf +sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf +sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf + +systemctl enable container-runtime.service + +rm -rf /run/systemd \ No newline at end of file diff --git a/container-runtime/download-base-image.sh b/container-runtime/download-base-image.sh new file mode 100755 index 000000000..7fb134f31 --- /dev/null +++ b/container-runtime/download-base-image.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e + +DISTRO=debian +VERSION=bookworm +ARCH=${ARCH:-$(uname -m)} +FLAVOR=default + +_ARCH=$ARCH +if [ "$_ARCH" = "x86_64" ]; then + _ARCH=amd64 +elif [ "$_ARCH" = "aarch64" ]; then + _ARCH=arm64 +fi + +URL="https://images.linuxcontainers.org/$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs" + +echo "Downloading $URL to debian.${ARCH}.squashfs" + +curl -fsSL "$URL" > debian.${ARCH}.squashfs \ No newline at end of file diff --git a/container-runtime/install-dist-deps.sh b/container-runtime/install-dist-deps.sh new file mode 100755 index 000000000..d155ed4f2 --- /dev/null +++ b/container-runtime/install-dist-deps.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e + +cat ./package.json | sed 's/file:\.\([.\/]\)/file:..\/.\1/g' > ./dist/package.json +cat ./package-lock.json | sed 's/"\.\([.\/]\)/"..\/.\1/g' > ./dist/package-lock.json + +npm --prefix dist ci --omit=dev \ No newline at end of file diff --git a/container-runtime/jest.config.js b/container-runtime/jest.config.js new file mode 100644 index 000000000..f499f03f9 --- /dev/null +++ b/container-runtime/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + automock: false, + testEnvironment: "node", + rootDir: "./src/", + modulePathIgnorePatterns: ["./dist/"], +} diff --git a/container-runtime/mkcontainer.sh b/container-runtime/mkcontainer.sh new file mode 100644 index 000000000..90de54671 --- /dev/null +++ b/container-runtime/mkcontainer.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +IMAGE=$1 + +if [ -z "$IMAGE" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +if ! [ -d "/media/images/$IMAGE" ]; then + >&2 echo "image does not exist" + exit 1 +fi + +container=$(mktemp -d) +mkdir -p $container/rootfs $container/upper $container/work +mount -t overlay -olowerdir=/media/images/$IMAGE,upperdir=$container/upper,workdir=$container/work overlay $container/rootfs + +rootfs=$container/rootfs + +for special in dev sys proc run; do + mkdir -p $rootfs/$special + mount --bind /$special $rootfs/$special +done + +echo $rootfs \ No newline at end of file diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json new file mode 100644 index 000000000..097c3ccb7 --- /dev/null +++ b/container-runtime/package-lock.json @@ -0,0 +1,9953 @@ +{ + "name": "container-runtime", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "container-runtime", + "version": "0.0.0", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@start9labs/start-sdk": "file:../sdk/dist", + "esbuild-plugin-resolve": "^2.0.0", + "filebrowser": "^1.0.0", + "isomorphic-fetch": "^3.0.0", + "jsonpath": "^1.1.1", + "lodash.merge": "^4.6.2", + "node-fetch": "^3.1.0", + "ts-matches": "^5.5.1", + "tslib": "^2.5.3", + "typescript": "^5.1.3", + "yaml": "^2.3.1" + }, + "devDependencies": { + "@swc/cli": "^0.1.62", + "@swc/core": "^1.3.65", + "@types/jest": "^29.5.12", + "@types/jsonpath": "^0.2.4", + "@types/node": "^20.11.13", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "ts-jest": "^29.2.3", + "typescript": ">5.2" + } + }, + "../sdk/baseDist": { + "name": "@start9labs/start-sdk-base", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } + }, + "../sdk/dist": { + "name": "@start9labs/start-sdk", + "version": "0.3.6-beta.4", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "license": "ISC" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@start9labs/start-sdk": { + "resolved": "../sdk/dist", + "link": true + }, + "node_modules/@swc/cli": { + "version": "0.1.65", + "dev": true, + "license": "MIT", + "dependencies": { + "@mole-inc/bin-wrapper": "^8.0.1", + "commander": "^7.1.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 12.13" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/core": { + "version": "1.5.28", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.28", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.28", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bin-check": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/bin-version/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/bin-version/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bin-version/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bin-version/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bin-version/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bin-version/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild-plugin-resolve": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.19.2", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "17.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filebrowser": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "commander": "^2.9.0", + "content-disposition": "^0.5.1", + "express": "^4.14.0" + } + }, + "node_modules/filebrowser/node_modules/commander": { + "version": "2.20.3", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/pump": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.11.0", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/trim-repeated": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-matches": { + "version": "5.5.1", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.6.3", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.4.5", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true + }, + "@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dev": true, + "requires": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true + }, + "@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@iarna/toml": { + "version": "2.2.5" + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mole-inc/bin-wrapper": { + "version": "8.0.1", + "dev": true, + "requires": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + } + }, + "@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sindresorhus/is": { + "version": "4.6.0", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@start9labs/start-sdk": { + "version": "file:../sdk/dist", + "requires": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "isomorphic-fetch": "^3.0.0", + "jest": "^29.4.3", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-matches": "^6.2.1", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4", + "yaml": "^2.2.2" + } + }, + "@swc/cli": { + "version": "0.1.65", + "dev": true, + "requires": { + "@mole-inc/bin-wrapper": "^8.0.1", + "commander": "^7.1.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + } + }, + "@swc/core": { + "version": "1.5.28", + "dev": true, + "requires": { + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28", + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + } + }, + "@swc/core-linux-x64-gnu": { + "version": "1.5.28", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.5.28", + "dev": true, + "optional": true + }, + "@swc/counter": { + "version": "0.1.3", + "dev": true + }, + "@swc/types": { + "version": "0.1.8", + "dev": true, + "requires": { + "@swc/counter": "^0.1.3" + } + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@tokenizer/token": { + "version": "0.3.0", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/cacheable-request": { + "version": "6.0.3", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.4", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "20.14.2", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/responselike": { + "version": "1.0.3", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arch": { + "version": "2.2.0", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1" + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "dev": true + }, + "bin-check": { + "version": "4.1.0", + "dev": true, + "requires": { + "execa": "^0.7.0", + "executable": "^4.1.0" + } + }, + "bin-version": { + "version": "6.0.0", + "dev": true, + "requires": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "5.1.1", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "bin-version-check": { + "version": "5.1.0", + "dev": true, + "requires": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + } + }, + "body-parser": { + "version": "1.20.2", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.3", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "bytes": { + "version": "3.1.2" + }, + "cacheable-lookup": { + "version": "5.0.4", + "dev": true + }, + "cacheable-request": { + "version": "7.0.4", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "call-bind": { + "version": "1.0.7", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "clone-response": { + "version": "1.0.3", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5" + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cookie": { + "version": "0.6.0" + }, + "cookie-signature": { + "version": "1.0.6" + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1" + }, + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "dev": true + } + } + }, + "dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "requires": {} + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "defer-to-connect": { + "version": "2.0.1", + "dev": true + }, + "define-data-property": { + "version": "1.1.4", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "depd": { + "version": "2.0.0" + }, + "destroy": { + "version": "1.2.0" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "ee-first": { + "version": "1.1.1" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2" + }, + "end-of-stream": { + "version": "1.4.4", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-define-property": { + "version": "1.0.0", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0" + }, + "esbuild-plugin-resolve": { + "version": "2.0.0" + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true + }, + "escape-html": { + "version": "1.0.3" + }, + "escape-string-regexp": { + "version": "5.0.0", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1" + }, + "execa": { + "version": "0.7.0", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "executable": { + "version": "4.1.1", + "dev": true, + "requires": { + "pify": "^2.2.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "express": { + "version": "4.19.2", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "ext-list": { + "version": "2.2.2", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "fast-glob": { + "version": "3.3.2", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fastq": { + "version": "1.17.1", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fetch-blob": { + "version": "3.2.0", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "file-type": { + "version": "17.1.6", + "dev": true, + "requires": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + } + }, + "filebrowser": { + "version": "1.0.0", + "requires": { + "commander": "^2.9.0", + "content-disposition": "^0.5.1", + "express": "^4.14.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3" + } + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "filename-reserved-regex": { + "version": "3.0.0", + "dev": true + }, + "filenamify": { + "version": "5.1.1", + "dev": true, + "requires": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + } + }, + "fill-range": { + "version": "7.1.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "find-versions": { + "version": "5.1.0", + "dev": true, + "requires": { + "semver-regex": "^4.0.5" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "forwarded": { + "version": "0.2.0" + }, + "fresh": { + "version": "0.5.2" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.4", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "gopd": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "got": { + "version": "11.8.6", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3" + }, + "has-symbols": { + "version": "1.0.3" + }, + "hasown": { + "version": "2.0.2", + "requires": { + "function-bind": "^1.1.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.1", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http2-wrapper": { + "version": "1.0.3", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "dev": true + }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "ipaddr.js": { + "version": "1.9.1" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "isomorphic-fetch": { + "version": "3.0.0", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==" + } + } + }, + "keyv": { + "version": "4.5.4", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lowercase-keys": { + "version": "2.0.0", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "media-typer": { + "version": "0.3.0" + }, + "merge-descriptors": { + "version": "1.0.1" + }, + "merge-stream": { + "version": "2.0.0", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "methods": { + "version": "1.1.2" + }, + "micromatch": { + "version": "4.0.7", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0" + }, + "mime-db": { + "version": "1.52.0" + }, + "mime-types": { + "version": "2.1.35", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "dev": true + }, + "minimatch": { + "version": "9.0.4", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.0.0" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "negotiator": { + "version": "0.6.3" + }, + "node-domexception": { + "version": "1.0.0" + }, + "node-fetch": { + "version": "3.3.2", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "object-inspect": { + "version": "1.13.1" + }, + "on-finished": { + "version": "2.4.1", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-filter-obj": { + "version": "2.0.0", + "dev": true, + "requires": { + "arch": "^2.1.0" + } + }, + "p-cancelable": { + "version": "2.1.1", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parseurl": { + "version": "1.3.3" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7" + }, + "peek-readable": { + "version": "5.0.0", + "dev": true + }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "dev": true + }, + "pify": { + "version": "2.3.0", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "prettier": { + "version": "3.3.2", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "proxy-addr": { + "version": "2.0.7", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "dev": true + }, + "pump": { + "version": "3.0.0", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "dev": true + }, + "range-parser": { + "version": "1.2.1" + }, + "raw-body": { + "version": "2.5.2", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.2", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "dev": true, + "requires": { + "readable-stream": "^3.6.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-alpn": { + "version": "1.2.1", + "dev": true + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safer-buffer": { + "version": "2.1.2" + }, + "semver": { + "version": "7.6.2", + "dev": true + }, + "semver-regex": { + "version": "4.0.5", + "dev": true + }, + "semver-truncate": { + "version": "3.0.0", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "send": { + "version": "0.18.0", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3" + } + } + }, + "serve-static": { + "version": "1.15.0", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0" + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "side-channel": { + "version": "1.0.6", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "sort-keys": { + "version": "1.1.2", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, + "source-map": { + "version": "0.7.4", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } + }, + "statuses": { + "version": "2.0.1" + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strip-outer": { + "version": "2.0.0", + "dev": true + }, + "strtok3": { + "version": "7.0.0", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1" + }, + "token-types": { + "version": "5.0.1", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, + "tr46": { + "version": "0.0.3" + }, + "trim-repeated": { + "version": "2.0.0", + "dev": true, + "requires": { + "escape-string-regexp": "^5.0.0" + } + }, + "ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + } + }, + "ts-matches": { + "version": "5.5.1" + }, + "tslib": { + "version": "2.6.3" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "5.4.5", + "dev": true + }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, + "undici-types": { + "version": "5.26.5", + "dev": true + }, + "unpipe": { + "version": "1.0.0" + }, + "update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "dev": true + }, + "utils-merge": { + "version": "1.0.1" + }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "vary": { + "version": "1.1.2" + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "web-streams-polyfill": { + "version": "3.3.3" + }, + "webidl-conversions": { + "version": "3.0.1" + }, + "whatwg-fetch": { + "version": "3.6.20" + }, + "whatwg-url": { + "version": "5.0.0", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "dev": true + }, + "yaml": { + "version": "2.4.5" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/container-runtime/package.json b/container-runtime/package.json new file mode 100644 index 000000000..0a8e4afa8 --- /dev/null +++ b/container-runtime/package.json @@ -0,0 +1,46 @@ +{ + "name": "container-runtime", + "version": "0.0.0", + "description": "We want to be the sdk intermitent for the system", + "module": "./index.js", + "scripts": { + "check": "tsc --noEmit", + "build": "prettier . '!tmp/**' --write && rm -rf dist && tsc", + "tsc": "rm -rf dist; tsc", + "test": "jest -c ./jest.config.js" + }, + "author": "", + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": false + }, + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@start9labs/start-sdk": "file:../sdk/dist", + "esbuild-plugin-resolve": "^2.0.0", + "filebrowser": "^1.0.0", + "isomorphic-fetch": "^3.0.0", + "jsonpath": "^1.1.1", + "lodash.merge": "^4.6.2", + "node-fetch": "^3.1.0", + "ts-matches": "^5.5.1", + "tslib": "^2.5.3", + "typescript": "^5.1.3", + "yaml": "^2.3.1" + }, + "devDependencies": { + "@swc/cli": "^0.1.62", + "@swc/core": "^1.3.65", + "@types/jest": "^29.5.12", + "@types/jsonpath": "^0.2.4", + "@types/node": "^20.11.13", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "ts-jest": "^29.2.3", + "typescript": ">5.2" + } +} diff --git a/container-runtime/rmcontainer.sh b/container-runtime/rmcontainer.sh new file mode 100644 index 000000000..69912eeba --- /dev/null +++ b/container-runtime/rmcontainer.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +rootfs=$1 +if [ -z "$rootfs" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +umount --recursive $rootfs +rm -rf $rootfs/.. \ No newline at end of file diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts new file mode 100644 index 000000000..4bda0ed5d --- /dev/null +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -0,0 +1,314 @@ +import { types as T, utils } from "@start9labs/start-sdk" +import * as net from "net" +import { object, string, number, literals, some, unknown } from "ts-matches" +import { Effects } from "../Models/Effects" + +import { CallbackHolder } from "../Models/CallbackHolder" +import { asError } from "@start9labs/start-sdk/base/lib/util" +const matchRpcError = object({ + error: object( + { + code: number, + message: string, + data: some( + string, + object( + { + details: string, + debug: string, + }, + ["debug"], + ), + ), + }, + ["data"], + ), +}) +const testRpcError = matchRpcError.test +const testRpcResult = object({ + result: unknown, +}).test +type RpcError = typeof matchRpcError._TYPE + +const SOCKET_PATH = "/media/startos/rpc/host.sock" +let hostSystemId = 0 + +export type EffectContext = { + procedureId: string | null + callbacks?: CallbackHolder + constRetry: () => void +} + +const rpcRoundFor = + (procedureId: string | null) => + ( + method: K, + params: Record, + ) => { + const id = hostSystemId++ + const client = net.createConnection({ path: SOCKET_PATH }, () => { + client.write( + JSON.stringify({ + id, + method, + params: { ...params, procedureId: procedureId || undefined }, + }) + "\n", + ) + }) + let bufs: Buffer[] = [] + return new Promise((resolve, reject) => { + client.on("data", (data) => { + try { + bufs.push(data) + if (data.reduce((acc, x) => acc || x == 10, false)) { + const res: unknown = JSON.parse( + Buffer.concat(bufs).toString().split("\n")[0], + ) + if (testRpcError(res)) { + let message = res.error.message + console.error( + "Error in host RPC:", + utils.asError({ method, params, error: res.error }), + ) + if (string.test(res.error.data)) { + message += ": " + res.error.data + console.error(`Details: ${res.error.data}`) + } else { + if (res.error.data?.details) { + message += ": " + res.error.data.details + console.error(`Details: ${res.error.data.details}`) + } + if (res.error.data?.debug) { + message += "\n" + res.error.data.debug + console.error(`Debug: ${res.error.data.debug}`) + } + } + reject(new Error(`${message}@${method}`)) + } else if (testRpcResult(res)) { + resolve(res.result) + } else { + reject(new Error(`malformed response ${JSON.stringify(res)}`)) + } + } + } catch (error) { + reject(error) + } + client.end() + }) + client.on("error", (error) => { + reject(error) + }) + }) + } + +export function makeEffects(context: EffectContext): Effects { + const rpcRound = rpcRoundFor(context.procedureId) + const self: Effects = { + constRetry: context.constRetry, + clearCallbacks(...[options]: Parameters) { + return rpcRound("clear-callbacks", { + ...options, + }) as ReturnType + }, + action: { + clear(...[options]: Parameters) { + return rpcRound("action.clear", { + ...options, + }) as ReturnType + }, + export(...[options]: Parameters) { + return rpcRound("action.export", { + ...options, + }) as ReturnType + }, + getInput(...[options]: Parameters) { + return rpcRound("action.get-input", { + ...options, + }) as ReturnType + }, + request(...[options]: Parameters) { + return rpcRound("action.request", { + ...options, + }) as ReturnType + }, + run(...[options]: Parameters) { + return rpcRound("action.run", { + ...options, + }) as ReturnType + }, + clearRequests( + ...[options]: Parameters + ) { + return rpcRound("action.clear-requests", { + ...options, + }) as ReturnType + }, + }, + bind(...[options]: Parameters) { + return rpcRound("bind", { + ...options, + stack: new Error().stack, + }) as ReturnType + }, + clearBindings(...[options]: Parameters) { + return rpcRound("clear-bindings", { ...options }) as ReturnType< + T.Effects["clearBindings"] + > + }, + clearServiceInterfaces( + ...[options]: Parameters + ) { + return rpcRound("clear-service-interfaces", { ...options }) as ReturnType< + T.Effects["clearServiceInterfaces"] + > + }, + getInstalledPackages(...[]: Parameters) { + return rpcRound("get-installed-packages", {}) as ReturnType< + T.Effects["getInstalledPackages"] + > + }, + subcontainer: { + createFs(options: { imageId: string; name: string }) { + return rpcRound("subcontainer.create-fs", options) as ReturnType< + T.Effects["subcontainer"]["createFs"] + > + }, + destroyFs(options: { guid: string }): Promise { + return rpcRound("subcontainer.destroy-fs", options) as ReturnType< + T.Effects["subcontainer"]["destroyFs"] + > + }, + }, + exportServiceInterface: (( + ...[options]: Parameters + ) => { + return rpcRound("export-service-interface", options) as ReturnType< + T.Effects["exportServiceInterface"] + > + }) as Effects["exportServiceInterface"], + exposeForDependents( + ...[options]: Parameters + ) { + return rpcRound("expose-for-dependents", options) as ReturnType< + T.Effects["exposeForDependents"] + > + }, + getContainerIp(...[]: Parameters) { + return rpcRound("get-container-ip", {}) as ReturnType< + T.Effects["getContainerIp"] + > + }, + getHostInfo: ((...[allOptions]: Parameters) => { + const options = { + ...allOptions, + callback: context.callbacks?.addCallback(allOptions.callback) || null, + } + return rpcRound("get-host-info", options) as ReturnType< + T.Effects["getHostInfo"] + > as any + }) as Effects["getHostInfo"], + getServiceInterface( + ...[options]: Parameters + ) { + return rpcRound("get-service-interface", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + + getServicePortForward( + ...[options]: Parameters + ) { + return rpcRound("get-service-port-forward", options) as ReturnType< + T.Effects["getServicePortForward"] + > + }, + getSslCertificate(options: Parameters[0]) { + return rpcRound("get-ssl-certificate", options) as ReturnType< + T.Effects["getSslCertificate"] + > + }, + getSslKey(options: Parameters[0]) { + return rpcRound("get-ssl-key", options) as ReturnType< + T.Effects["getSslKey"] + > + }, + getSystemSmtp(...[options]: Parameters) { + return rpcRound("get-system-smtp", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + listServiceInterfaces( + ...[options]: Parameters + ) { + return rpcRound("list-service-interfaces", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + mount(...[options]: Parameters) { + return rpcRound("mount", options) as ReturnType + }, + restart(...[]: Parameters) { + return rpcRound("restart", {}) as ReturnType + }, + setDependencies( + dependencies: Parameters[0], + ): ReturnType { + return rpcRound("set-dependencies", dependencies) as ReturnType< + T.Effects["setDependencies"] + > + }, + checkDependencies( + options: Parameters[0], + ): ReturnType { + return rpcRound("check-dependencies", options) as ReturnType< + T.Effects["checkDependencies"] + > + }, + getDependencies(): ReturnType { + return rpcRound("get-dependencies", {}) as ReturnType< + T.Effects["getDependencies"] + > + }, + setHealth(...[options]: Parameters) { + return rpcRound("set-health", options) as ReturnType< + T.Effects["setHealth"] + > + }, + + getStatus(...[o]: Parameters) { + return rpcRound("get-status", o) as ReturnType + }, + setMainStatus(o: { status: "running" | "stopped" }): Promise { + return rpcRound("set-main-status", o) as ReturnType< + T.Effects["setHealth"] + > + }, + + shutdown(...[]: Parameters) { + return rpcRound("shutdown", {}) as ReturnType + }, + store: { + get: async (options: any) => + rpcRound("store.get", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as any, + set: async (options: any) => + rpcRound("store.set", options) as ReturnType, + } as T.Effects["store"], + getDataVersion() { + return rpcRound("get-data-version", {}) as ReturnType< + T.Effects["getDataVersion"] + > + }, + setDataVersion(...[options]: Parameters) { + return rpcRound("set-data-version", options) as ReturnType< + T.Effects["setDataVersion"] + > + }, + } + return self +} diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts new file mode 100644 index 000000000..c2dc8bafe --- /dev/null +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -0,0 +1,491 @@ +// @ts-check + +import * as net from "net" +import { + object, + some, + string, + literal, + array, + number, + matches, + any, + shape, + anyOf, +} from "ts-matches" + +import { types as T, utils } from "@start9labs/start-sdk" +import * as fs from "fs" + +import { CallbackHolder } from "../Models/CallbackHolder" +import { AllGetDependencies } from "../Interfaces/AllGetDependencies" +import { jsonPath, unNestPath } from "../Models/JsonPath" +import { System } from "../Interfaces/System" +import { makeEffects } from "./EffectCreator" +type MaybePromise = T | Promise +export const matchRpcResult = anyOf( + object({ result: any }), + object({ + error: object( + { + code: number, + message: string, + data: object( + { + details: string, + debug: any, + }, + ["details", "debug"], + ), + }, + ["data"], + ), + }), +) + +export type RpcResult = typeof matchRpcResult._TYPE +type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null + +const SOCKET_PARENT = "/media/startos/rpc" +const SOCKET_PATH = "/media/startos/rpc/service.sock" +const jsonrpc = "2.0" as const + +const isResult = object({ result: any }).test + +const idType = some(string, number, literal(null)) +type IdType = null | string | number | undefined +const runType = object( + { + id: idType, + method: literal("execute"), + params: object( + { + id: string, + procedure: string, + input: any, + timeout: number, + }, + ["timeout"], + ), + }, + ["id"], +) +const sandboxRunType = object( + { + id: idType, + method: literal("sandbox"), + params: object( + { + id: string, + procedure: string, + input: any, + timeout: number, + }, + ["timeout"], + ), + }, + ["id"], +) +const callbackType = object({ + method: literal("callback"), + params: object({ + id: number, + args: array, + }), +}) +const initType = object( + { + id: idType, + method: literal("init"), + }, + ["id"], +) +const startType = object( + { + id: idType, + method: literal("start"), + }, + ["id"], +) +const stopType = object( + { + id: idType, + method: literal("stop"), + }, + ["id"], +) +const exitType = object( + { + id: idType, + method: literal("exit"), + }, + ["id"], +) +const evalType = object( + { + id: idType, + method: literal("eval"), + params: object({ + script: string, + }), + }, + ["id"], +) + +const jsonParse = (x: string) => JSON.parse(x) + +const handleRpc = (id: IdType, result: Promise) => + result + .then((result) => { + return { + jsonrpc, + id, + ...result, + } + }) + .then((x) => { + if ( + ("result" in x && x.result === undefined) || + !("error" in x || "result" in x) + ) + (x as any).result = null + return x + }) + .catch((error) => ({ + jsonrpc, + id, + error: { + code: 0, + message: typeof error, + data: { details: "" + error, debug: error?.stack }, + }, + })) + +const hasId = object({ id: idType }).test +export class RpcListener { + unixSocketServer = net.createServer(async (server) => {}) + private _system: System | undefined + private callbacks: CallbackHolder | undefined + + constructor(readonly getDependencies: AllGetDependencies) { + if (!fs.existsSync(SOCKET_PARENT)) { + fs.mkdirSync(SOCKET_PARENT, { recursive: true }) + } + this.unixSocketServer.listen(SOCKET_PATH) + + this.unixSocketServer.on("connection", (s) => { + let id: IdType = null + const captureId = (x: X) => { + if (hasId(x)) id = x.id + return x + } + const logData = + (location: string) => + (x: X) => { + console.log({ + location, + stringified: JSON.stringify(x), + type: typeof x, + id, + }) + return x + } + const mapError = (error: any): SocketResponse => ({ + jsonrpc, + id, + error: { + message: typeof error, + data: { + details: error?.message ?? String(error), + debug: error?.stack, + }, + code: 1, + }, + }) + const writeDataToSocket = (x: SocketResponse) => { + if (x != null) { + return new Promise((resolve) => + s.write(JSON.stringify(x) + "\n", resolve), + ) + } + } + s.on("data", (a) => + Promise.resolve(a) + .then((b) => b.toString()) + .then((buf) => { + for (let s of buf.split("\n")) { + if (s) + Promise.resolve(s) + .then(logData("dataIn")) + .then(jsonParse) + .then(captureId) + .then((x) => this.dealWithInput(x)) + .catch(mapError) + .then(logData("response")) + .then(writeDataToSocket) + .catch((e) => { + console.error(`Major error in socket handling: ${e}`) + console.debug(`Data in: ${a.toString()}`) + }) + } + }), + ) + }) + } + + private get system() { + if (!this._system) throw new Error("System not initialized") + return this._system + } + + private callbackHolders: Map = new Map() + private removeCallbackHolderFor(procedure: string) { + const prev = this.callbackHolders.get(procedure) + if (prev) { + this.callbackHolders.delete(procedure) + this.callbacks?.removeChild(prev) + } + } + private callbackHolderFor(procedure: string): CallbackHolder { + this.removeCallbackHolderFor(procedure) + const callbackHolder = this.callbacks!.child() + this.callbackHolders.set(procedure, callbackHolder) + return callbackHolder + } + + callCallback(callback: number, args: any[]): void { + if (this.callbacks) { + this.callbacks + .callCallback(callback, args) + .catch((error) => + console.error(`callback ${callback} failed`, utils.asError(error)), + ) + } else { + console.warn( + `callback ${callback} ignored because system is not initialized`, + ) + } + } + + private dealWithInput(input: unknown): MaybePromise { + return matches(input) + .when(runType, async ({ id, params }) => { + const system = this.system + const procedure = jsonPath.unsafeCast(params.procedure) + const { input, timeout, id: procedureId } = params + const result = this.getResult( + procedure, + system, + procedureId, + timeout, + input, + ) + + return handleRpc(id, result) + }) + .when(sandboxRunType, async ({ id, params }) => { + const system = this.system + const procedure = jsonPath.unsafeCast(params.procedure) + const { input, timeout, id: procedureId } = params + const result = this.getResult( + procedure, + system, + procedureId, + timeout, + input, + ) + + return handleRpc(id, result) + }) + .when(callbackType, async ({ params: { id, args } }) => { + this.callCallback(id, args) + return null + }) + .when(startType, async ({ id }) => { + const callbacks = this.callbackHolderFor("main") + const effects = makeEffects({ + procedureId: null, + callbacks, + constRetry: () => {}, + }) + return handleRpc( + id, + this.system.start(effects).then((result) => ({ result })), + ) + }) + .when(stopType, async ({ id }) => { + this.removeCallbackHolderFor("main") + return handleRpc( + id, + this.system.stop().then((result) => ({ result })), + ) + }) + .when(exitType, async ({ id }) => { + return handleRpc( + id, + (async () => { + if (this._system) await this._system.exit() + })().then((result) => ({ result })), + ) + }) + .when(initType, async ({ id }) => { + return handleRpc( + id, + (async () => { + if (!this._system) { + const system = await this.getDependencies.system() + this.callbacks = new CallbackHolder( + makeEffects({ + procedureId: null, + constRetry: () => {}, + }), + ) + const callbacks = this.callbackHolderFor("containerInit") + await system.containerInit( + makeEffects({ + procedureId: null, + callbacks, + constRetry: () => {}, + }), + ) + this._system = system + } + })().then((result) => ({ result })), + ) + }) + .when(evalType, async ({ id, params }) => { + return handleRpc( + id, + (async () => { + const result = await new Function( + `return (async () => { return (${params.script}) }).call(this)`, + ).call({ + listener: this, + require: require, + }) + return { + jsonrpc, + id, + result: ![ + "string", + "number", + "boolean", + "null", + "object", + ].includes(typeof result) + ? null + : result, + } + })(), + ) + }) + .when( + shape({ id: idType, method: string }, ["id"]), + ({ id, method }) => ({ + jsonrpc, + id, + error: { + code: -32601, + message: `Method not found`, + data: { + details: method, + }, + }, + }), + ) + + .defaultToLazy(() => { + console.warn( + `Couldn't parse the following input ${JSON.stringify(input)}`, + ) + return { + jsonrpc, + id: (input as any)?.id, + error: { + code: -32602, + message: "invalid params", + data: { + details: JSON.stringify(input), + }, + }, + } + }) + } + private getResult( + procedure: typeof jsonPath._TYPE, + system: System, + procedureId: string, + timeout: number | undefined, + input: any, + ) { + const ensureResultTypeShape = ( + result: void | T.ActionInput | T.ActionResult | null, + ): { result: any } => { + return { result } + } + const callbacks = this.callbackHolderFor(procedure) + const effects = makeEffects({ + procedureId, + callbacks, + constRetry: () => {}, + }) + + return (async () => { + switch (procedure) { + case "/backup/create": + return system.createBackup(effects, timeout || null) + case "/backup/restore": + return system.restoreBackup(effects, timeout || null) + case "/packageInit": + return system.packageInit(effects, timeout || null) + case "/packageUninit": + return system.packageUninit( + effects, + string.optional().unsafeCast(input), + timeout || null, + ) + default: + const procedures = unNestPath(procedure) + switch (true) { + case procedures[1] === "actions" && procedures[3] === "getInput": + return system.getActionInput( + effects, + procedures[2], + timeout || null, + ) + case procedures[1] === "actions" && procedures[3] === "run": + return system.runAction( + effects, + procedures[2], + input.input, + timeout || null, + ) + } + } + })().then(ensureResultTypeShape, (error) => + matches(error) + .when( + object( + { + error: string, + code: number, + }, + ["code"], + { code: 0 }, + ), + (error) => ({ + error: { + code: error.code, + message: error.error, + }, + }), + ) + .defaultToLazy(() => ({ + error: { + code: 0, + message: String(error), + }, + })), + ) + } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts new file mode 100644 index 000000000..806216786 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -0,0 +1,157 @@ +import * as fs from "fs/promises" +import * as cp from "child_process" +import { SubContainer, types as T } from "@start9labs/start-sdk" +import { promisify } from "util" +import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure" +import { Volume } from "./matchVolume" +import { + CommandOptions, + ExecOptions, + ExecSpawnable, +} from "@start9labs/start-sdk/package/lib/util/SubContainer" +export const exec = promisify(cp.exec) +export const execFile = promisify(cp.execFile) + +export class DockerProcedureContainer { + private constructor(private readonly subcontainer: ExecSpawnable) {} + + static async of( + effects: T.Effects, + packageId: string, + data: DockerProcedure, + volumes: { [id: VolumeId]: Volume }, + name: string, + options: { subcontainer?: ExecSpawnable } = {}, + ) { + const subcontainer = + options?.subcontainer ?? + (await DockerProcedureContainer.createSubContainer( + effects, + packageId, + data, + volumes, + name, + )) + return new DockerProcedureContainer(subcontainer) + } + static async createSubContainer( + effects: T.Effects, + packageId: string, + data: DockerProcedure, + volumes: { [id: VolumeId]: Volume }, + name: string, + ) { + const subcontainer = await SubContainer.of( + effects, + { imageId: data.image }, + name, + ) + + if (data.mounts) { + const mounts = data.mounts + for (const mount in mounts) { + const path = mounts[mount].startsWith("/") + ? `${subcontainer.rootfs}${mounts[mount]}` + : `${subcontainer.rootfs}/${mounts[mount]}` + await fs.mkdir(path, { recursive: true }) + const volumeMount = volumes[mount] + if (volumeMount.type === "data") { + await subcontainer.mount( + { type: "volume", id: mount, subpath: null, readonly: false }, + mounts[mount], + ) + } else if (volumeMount.type === "assets") { + await subcontainer.mount( + { type: "assets", id: mount, subpath: null }, + mounts[mount], + ) + } else if (volumeMount.type === "certificate") { + const hostnames = [ + `${packageId}.embassy`, + ...new Set( + Object.values( + ( + await effects.getHostInfo({ + hostId: volumeMount["interface-id"], + }) + )?.hostnameInfo || {}, + ) + .flatMap((h) => h) + .flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])), + ).values(), + ] + const certChain = await effects.getSslCertificate({ + hostnames, + }) + const key = await effects.getSslKey({ + hostnames, + }) + await fs.writeFile( + `${path}/${volumeMount["interface-id"]}.cert.pem`, + certChain.join("\n"), + ) + await fs.writeFile( + `${path}/${volumeMount["interface-id"]}.key.pem`, + key, + ) + } else if (volumeMount.type === "pointer") { + await effects + .mount({ + location: path, + target: { + packageId: volumeMount["package-id"], + subpath: volumeMount.path, + readonly: volumeMount.readonly, + volumeId: volumeMount["volume-id"], + }, + }) + .catch(console.warn) + } else if (volumeMount.type === "backup") { + await subcontainer.mount( + { type: "backup", subpath: null }, + mounts[mount], + ) + } + } + } + return subcontainer + } + + async exec( + commands: string[], + options?: CommandOptions & ExecOptions, + timeoutMs?: number | null, + ) { + try { + return await this.subcontainer.exec(commands, options, timeoutMs) + } finally { + await this.subcontainer.destroy?.() + } + } + + async execFail( + commands: string[], + timeoutMs: number | null, + options?: CommandOptions & ExecOptions, + ) { + try { + const res = await this.subcontainer.exec(commands, options, timeoutMs) + if (res.exitCode !== 0) { + const codeOrSignal = + res.exitCode !== null + ? `code ${res.exitCode}` + : `signal ${res.exitSignal}` + throw new Error( + `Process exited with ${codeOrSignal}: ${res.stderr.toString()}`, + ) + } + return res + } finally { + await this.subcontainer.destroy?.() + } + } + + async spawn(commands: string[]): Promise { + return await this.subcontainer.spawn(commands) + } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts new file mode 100644 index 000000000..fa76a1f84 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -0,0 +1,346 @@ +import { polyfillEffects } from "./polyfillEffects" +import { DockerProcedureContainer } from "./DockerProcedureContainer" +import { SystemForEmbassy } from "." +import { T, utils } from "@start9labs/start-sdk" +import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon" +import { Effects } from "../../../Models/Effects" +import { off } from "node:process" +import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController" + +const EMBASSY_HEALTH_INTERVAL = 15 * 1000 +const EMBASSY_PROPERTIES_LOOP = 30 * 1000 +/** + * We wanted something to represent what the main loop is doing, and + * in this case it used to run the properties, health, and the docker/ js main. + * Also, this has an ability to clean itself up too if need be. + */ +export class MainLoop { + get mainSubContainerHandle() { + return this.mainEvent?.daemon?.subContainerHandle + } + private healthLoops?: { + name: string + interval: NodeJS.Timeout + }[] + + private mainEvent?: { + daemon: Daemon + } + + private constructor( + readonly system: SystemForEmbassy, + readonly effects: Effects, + ) {} + + static async of( + system: SystemForEmbassy, + effects: Effects, + ): Promise { + const res = new MainLoop(system, effects) + res.healthLoops = res.constructHealthLoops() + res.mainEvent = await res.constructMainEvent() + return res + } + + private async constructMainEvent() { + const { system, effects } = this + const currentCommand: [string, ...string[]] = [ + system.manifest.main.entrypoint, + ...system.manifest.main.args, + ] + + await this.setupInterfaces(effects) + await effects.setMainStatus({ status: "running" }) + const jsMain = (this.system.moduleCode as any)?.jsMain + if (jsMain) { + throw new Error("Unreachable") + } + const daemon = new Daemon(async () => { + const subcontainer = await DockerProcedureContainer.createSubContainer( + effects, + this.system.manifest.id, + this.system.manifest.main, + this.system.manifest.volumes, + `Main - ${currentCommand.join(" ")}`, + ) + return CommandController.of()( + this.effects, + subcontainer, + currentCommand, + { + runAsInit: true, + env: { + TINI_SUBREAPER: "true", + }, + sigtermTimeout: utils.inMs( + this.system.manifest.main["sigterm-timeout"], + ), + }, + ) + }) + + daemon.start() + return { + daemon, + } + } + + private async setupInterfaces(effects: T.Effects) { + for (const interfaceId in this.system.manifest.interfaces) { + const iface = this.system.manifest.interfaces[interfaceId] + const internalPorts = new Set() + for (const port of Object.values( + iface["tor-config"]?.["port-mapping"] || {}, + )) { + internalPorts.add(parseInt(port)) + } + for (const port of Object.values(iface["lan-config"] || {})) { + internalPorts.add(port.internal) + } + for (const internalPort of internalPorts) { + const torConf = Object.entries( + iface["tor-config"]?.["port-mapping"] || {}, + ) + .map(([external, internal]) => ({ + internal: parseInt(internal), + external: parseInt(external), + })) + .find((conf) => conf.internal == internalPort) + const lanConf = Object.entries(iface["lan-config"] || {}) + .map(([external, conf]) => ({ + external: parseInt(external), + ...conf, + })) + .find((conf) => conf.internal == internalPort) + await effects.bind({ + id: interfaceId, + internalPort, + preferredExternalPort: torConf?.external || internalPort, + secure: null, + addSsl: lanConf?.ssl + ? { + preferredExternalPort: lanConf.external, + alpn: { specified: ["http/1.1"] }, + } + : null, + }) + } + } + } + + public async clean(options?: { timeout?: number }) { + const { mainEvent, healthLoops } = this + const main = await mainEvent + delete this.mainEvent + delete this.healthLoops + await main?.daemon + .stop() + .catch((e: unknown) => console.error(`Main loop error`, utils.asError(e))) + this.effects.setMainStatus({ status: "stopped" }) + if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) + } + + private constructHealthLoops() { + const { manifest } = this.system + const effects = this.effects + const start = Date.now() + return Object.entries(manifest["health-checks"]).map( + ([healthId, value]) => { + effects + .setHealth({ + id: healthId, + name: value.name, + result: "starting", + message: null, + }) + .catch((e) => console.error(utils.asError(e))) + const interval = setInterval(async () => { + const actionProcedure = value + const timeChanged = Date.now() - start + if (actionProcedure.type === "docker") { + const subcontainer = actionProcedure.inject + ? this.mainSubContainerHandle + : undefined + const commands = [ + actionProcedure.entrypoint, + ...actionProcedure.args, + ] + const container = await DockerProcedureContainer.of( + effects, + manifest.id, + actionProcedure, + manifest.volumes, + `Health Check - ${commands.join(" ")}`, + { + subcontainer, + }, + ) + const env: Record = actionProcedure.inject + ? { + HOME: "/root", + } + : {} + const executed = await container.exec(commands, { + input: JSON.stringify(timeChanged), + env, + }) + + if (executed.exitCode === 0) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "success", + message: actionProcedure["success-message"] ?? null, + }) + return + } + if (executed.exitCode === 59) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "disabled", + message: + executed.stderr.toString() || executed.stdout.toString(), + }) + return + } + if (executed.exitCode === 60) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "starting", + message: + executed.stderr.toString() || executed.stdout.toString(), + }) + return + } + if (executed.exitCode === 61) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "loading", + message: + executed.stderr.toString() || executed.stdout.toString(), + }) + return + } + const errorMessage = executed.stderr.toString() + const message = executed.stdout.toString() + if (!!errorMessage) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: errorMessage, + }) + return + } + if (executed.exitCode && executed.exitCode > 0) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: + executed.stderr.toString() || + executed.stdout.toString() || + `Program exited with code ${executed.exitCode}:`, + }) + return + } + await effects.setHealth({ + id: healthId, + name: value.name, + result: "success", + message, + }) + return + } else { + actionProcedure + const moduleCode = await this.system.moduleCode + const method = moduleCode.health?.[healthId] + if (!method) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: `Expecting that the js health check ${healthId} exists`, + }) + return + } + + const result = await method( + polyfillEffects(effects, this.system.manifest), + timeChanged, + ) + + if ("result" in result) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "success", + message: null, + }) + return + } + if ("error" in result) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: result.error, + }) + return + } + if (!("error-code" in result)) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: `Unknown error type ${JSON.stringify(result)}`, + }) + return + } + const [code, message] = result["error-code"] + if (code === 59) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "disabled", + message, + }) + return + } + if (code === 60) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "starting", + message, + }) + return + } + if (code === 61) { + await effects.setHealth({ + id: healthId, + name: value.name, + result: "loading", + message, + }) + return + } + + await effects.setHealth({ + id: healthId, + name: value.name, + result: "failure", + message: `${result["error-code"][0]}: ${result["error-code"][1]}`, + }) + return + } + }, EMBASSY_HEALTH_INTERVAL) + + return { name: healthId, interval } + }, + ) + } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts new file mode 100644 index 000000000..9a643b39d --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts @@ -0,0 +1,387 @@ +export default { + "peer-tor-address": { + name: "Peer Tor Address", + description: "The Tor address of the peer interface", + type: "pointer", + subtype: "package", + "package-id": "bitcoind", + target: "tor-address", + interface: "peer", + }, + "rpc-tor-address": { + name: "RPC Tor Address", + description: "The Tor address of the RPC interface", + type: "pointer", + subtype: "package", + "package-id": "bitcoind", + target: "tor-address", + interface: "rpc", + }, + rpc: { + type: "object", + name: "RPC Settings", + description: "RPC configuration options.", + spec: { + enable: { + type: "boolean", + name: "Enable", + description: "Allow remote RPC requests.", + default: true, + }, + username: { + type: "string", + nullable: false, + name: "Username", + description: "The username for connecting to Bitcoin over RPC.", + warning: + "You will need to restart all services that depend on Bitcoin.", + default: "bitcoin", + masked: true, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": "Must be alphanumeric (can contain underscore).", + }, + password: { + type: "string", + nullable: false, + name: "RPC Password", + description: "The password for connecting to Bitcoin over RPC.", + warning: + "You will need to restart all services that depend on Bitcoin.", + default: { + charset: "a-z,2-7", + len: 20, + }, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": "Must be alphanumeric (can contain underscore).", + copyable: true, + masked: true, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced RPC Settings", + spec: { + auth: { + name: "Authorization", + description: + "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + type: "list", + subtype: "string", + default: [], + spec: { + pattern: "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + "pattern-description": + 'Each item must be of the form ":$".', + }, + range: "[0,*)", + }, + servertimeout: { + name: "Rpc Server Timeout", + description: + "Number of seconds after which an uncompleted RPC call will time out.", + type: "number", + nullable: false, + range: "[5,300]", + integral: true, + units: "seconds", + default: 30, + }, + threads: { + name: "Threads", + description: + "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + type: "number", + nullable: false, + default: 16, + range: "[1,64]", + integral: true, + units: undefined, + }, + workqueue: { + name: "Work Queue", + description: + "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + type: "number", + nullable: false, + default: 128, + range: "[8,256]", + integral: true, + units: "requests", + }, + }, + }, + }, + }, + "zmq-enabled": { + type: "boolean", + name: "ZeroMQ Enabled", + description: + "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", + default: true, + }, + txindex: { + type: "boolean", + name: "Transaction Index", + description: + "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.", + default: true, + }, + coinstatsindex: { + type: "boolean", + name: "Coinstats Index", + description: + "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space", + default: false, + }, + wallet: { + type: "object", + name: "Wallet", + description: "Wallet Settings", + spec: { + enable: { + name: "Enable Wallet", + description: "Load the wallet and enable wallet RPC calls.", + type: "boolean", + default: true, + }, + avoidpartialspends: { + name: "Avoid Partial Spends", + description: + "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + type: "boolean", + default: true, + }, + discardfee: { + name: "Discard Change Tolerance", + description: + "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + type: "number", + nullable: false, + default: 0.0001, + range: "[0,.01]", + integral: false, + units: "BTC/kB", + }, + }, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced Settings", + spec: { + mempool: { + type: "object", + name: "Mempool", + description: "Mempool Settings", + spec: { + persistmempool: { + type: "boolean", + name: "Persist Mempool", + description: "Save the mempool on shutdown and load on restart.", + default: true, + }, + maxmempool: { + type: "number", + nullable: false, + name: "Max Mempool Size", + description: + "Keep the transaction memory pool below megabytes.", + range: "[1,*)", + integral: true, + units: "MiB", + default: 300, + }, + mempoolexpiry: { + type: "number", + nullable: false, + name: "Mempool Expiration", + description: + "Do not keep transactions in the mempool longer than hours.", + range: "[1,*)", + integral: true, + units: "Hr", + default: 336, + }, + mempoolfullrbf: { + name: "Enable Full RBF", + description: + "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies", + type: "boolean", + default: true, + }, + permitbaremultisig: { + type: "boolean", + name: "Permit Bare Multisig", + description: "Relay non-P2SH multisig transactions", + default: true, + }, + datacarrier: { + type: "boolean", + name: "Relay OP_RETURN Transactions", + description: "Relay transactions with OP_RETURN outputs", + default: true, + }, + datacarriersize: { + type: "number", + nullable: false, + name: "Max OP_RETURN Size", + description: "Maximum size of data in OP_RETURN outputs to relay", + range: "[0,10000]", + integral: true, + units: "bytes", + default: 83, + }, + }, + }, + peers: { + type: "object", + name: "Peers", + description: "Peer Connection Settings", + spec: { + listen: { + type: "boolean", + name: "Make Public", + description: + "Allow other nodes to find your server on the network.", + default: true, + }, + onlyconnect: { + type: "boolean", + name: "Disable Peer Discovery", + description: "Only connect to specified peers.", + default: false, + }, + onlyonion: { + type: "boolean", + name: "Disable Clearnet", + description: "Only connect to peers over Tor.", + default: false, + }, + v2transport: { + type: "boolean", + name: "Use V2 P2P Transport Protocol", + description: + "Enable or disable the use of BIP324 V2 P2P transport protocol.", + default: false, + }, + addnode: { + name: "Add Nodes", + description: "Add addresses of nodes to connect to.", + type: "list", + subtype: "object", + range: "[0,*)", + default: [], + spec: { + spec: { + hostname: { + type: "string", + nullable: false, + name: "Hostname", + description: "Domain or IP address of bitcoin peer", + pattern: + "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + "pattern-description": + "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + }, + port: { + type: "number", + nullable: true, + name: "Port", + description: + "Port that peer is listening on for inbound p2p connections", + range: "[0,65535]", + integral: true, + }, + }, + }, + }, + }, + }, + pruning: { + type: "union", + name: "Pruning Settings", + description: + "Blockchain Pruning Options\nReduce the blockchain size on disk\n", + warning: + "Disabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days.\n", + tag: { + id: "mode", + name: "Pruning Mode", + description: + "- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n", + "variant-names": { + disabled: "Disabled", + automatic: "Automatic", + }, + }, + variants: { + disabled: {}, + automatic: { + size: { + type: "number", + nullable: false, + name: "Max Chain Size", + description: "Limit of blockchain size on disk.", + warning: + "Increasing this value will require re-syncing your node.", + default: 550, + range: "[550,1000000)", + integral: true, + units: "MiB", + }, + }, + }, + default: "disabled", + }, + dbcache: { + type: "number", + nullable: true, + name: "Database Cache", + description: + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + warning: + "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + range: "(0,*)", + integral: true, + units: "MiB", + }, + blockfilters: { + type: "object", + name: "Block Filters", + description: "Settings for storing and serving compact block filters", + spec: { + blockfilterindex: { + type: "boolean", + name: "Compute Compact Block Filters (BIP158)", + description: + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + default: true, + }, + peerblockfilters: { + type: "boolean", + name: "Serve Compact Block Filters to Peers (BIP157)", + description: + "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + default: false, + }, + }, + }, + bloomfilters: { + type: "object", + name: "Bloom Filters (BIP37)", + description: "Setting for serving Bloom Filters", + spec: { + peerbloomfilters: { + type: "boolean", + name: "Serve Bloom Filters to Peers", + description: + "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", + warning: + "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", + default: false, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts new file mode 100644 index 000000000..cb70bd123 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts @@ -0,0 +1,127 @@ +export default { + homepage: { + name: "Homepage", + description: + "The page that will be displayed when your Start9 Pages .onion address is visited. Since this page is technically publicly accessible, you can choose to which type of page to display.", + type: "union", + default: "welcome", + tag: { + id: "type", + name: "Type", + "variant-names": { + welcome: "Welcome", + index: "Table of Contents", + "web-page": "Web Page", + redirect: "Redirect", + }, + }, + variants: { + welcome: {}, + index: {}, + "web-page": { + source: { + name: "Folder Location", + description: "The service that contains your website files.", + type: "enum", + values: ["filebrowser", "nextcloud"], + "value-names": {}, + default: "nextcloud", + }, + folder: { + type: "string", + name: "Folder Path", + placeholder: "e.g. websites/resume", + description: + 'The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.', + pattern: + "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + "pattern-description": "Must be a valid relative file path", + nullable: false, + }, + }, + redirect: { + target: { + type: "string", + name: "Target Subdomain", + description: + "The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.", + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens.", + nullable: false, + }, + }, + }, + }, + subdomains: { + type: "list", + name: "Subdomains", + description: "The websites you want to serve.", + default: [], + range: "[0, *)", + subtype: "object", + spec: { + "unique-by": "name", + "display-as": "{{name}}", + spec: { + name: { + type: "string", + nullable: false, + name: "Subdomain name", + description: + 'The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.', + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens", + }, + settings: { + type: "union", + name: "Settings", + description: + "The desired behavior you want to occur when the subdomain is visited. You can either redirect to another subdomain, or load a stored web page.", + default: "web-page", + tag: { + id: "type", + name: "Type", + "variant-names": { "web-page": "Web Page", redirect: "Redirect" }, + }, + variants: { + "web-page": { + source: { + name: "Folder Location", + description: "The service that contains your website files.", + type: "enum", + values: ["filebrowser", "nextcloud"], + "value-names": {}, + default: "nextcloud", + }, + folder: { + type: "string", + name: "Folder Path", + placeholder: "e.g. websites/resume", + description: + 'The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.', + pattern: + "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + "pattern-description": "Must be a valid relative file path", + nullable: false, + }, + }, + redirect: { + target: { + type: "string", + name: "Target Subdomain", + description: + "The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.", + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens.", + nullable: false, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts new file mode 100644 index 000000000..1b3a8ba94 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts @@ -0,0 +1,123 @@ +export default { + "eos-version": "0.3.5.1", + id: "gitea", + "git-hash": "91fada3edf30357a2e75c281d32f8888c87fcc2d\n", + title: "Gitea", + version: "1.22.0", + description: { + short: "A painless self-hosted Git service.", + long: "Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.\n", + }, + assets: { + license: "LICENSE", + instructions: "instructions.md", + icon: "icon.png", + "docker-images": null, + assets: null, + scripts: null, + }, + build: ["make"], + "release-notes": + "* Upstream code update\n* Fix deprecated config options\n* Full list of upstream changes available [here](https://github.com/go-gitea/gitea/compare/v1.21.8...v1.22.0)\n", + license: "MIT", + "wrapper-repo": "https://github.com/Start9Labs/gitea-startos", + "upstream-repo": "https://github.com/go-gitea/gitea", + "support-site": "https://docs.gitea.io/en-us/", + "marketing-site": "https://gitea.io/en-us/", + "donation-url": null, + alerts: { + install: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + main: { + type: "docker", + image: "main", + system: false, + entrypoint: "/usr/local/bin/docker_entrypoint.sh", + args: [], + inject: false, + mounts: { main: "/data" }, + "io-format": null, + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + "health-checks": { + "user-signups-off": { + name: "User Signups Off", + "success-message": null, + type: "script", + args: [], + timeout: null, + }, + web: { + name: "Web & Git HTTP Tor Interfaces", + "success-message": + "Gitea is ready to be visited in a web browser and git can be used with SSH over TOR.", + type: "script", + args: [], + timeout: null, + }, + }, + config: { + get: { type: "script", args: [] }, + set: { type: "script", args: [] }, + }, + properties: { type: "script", args: [] }, + volumes: { main: { type: "data" } }, + interfaces: { + main: { + name: "Web UI / Git HTTPS/SSH", + description: + "Port 80: Browser Interface and HTTP Git Interface / Port 22: Git SSH Interface", + "tor-config": { "port-mapping": { "22": "22", "80": "3000" } }, + "lan-config": { "443": { ssl: true, internal: 3000 } }, + ui: true, + protocols: ["tcp", "http", "ssh", "git"], + }, + }, + backup: { + create: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "create", "/mnt/backup", "/root/data"], + inject: false, + mounts: { BACKUP: "/mnt/backup", main: "/root/data" }, + "io-format": "yaml", + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + restore: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "restore", "/mnt/backup", "/root/data"], + inject: false, + mounts: { BACKUP: "/mnt/backup", main: "/root/data" }, + "io-format": "yaml", + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + }, + migrations: { + from: { "*": { type: "script", args: ["from"] } }, + to: { "*": { type: "script", args: ["to"] } }, + }, + actions: {}, + dependencies: {}, + containers: null, + replaces: [], + "hardware-requirements": { + device: {}, + ram: null, + arch: ["x86_64", "aarch64"], + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts new file mode 100644 index 000000000..f5a93a918 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts @@ -0,0 +1,28 @@ +export default { + "tor-address": { + name: "Tor Address", + description: "The Tor address of the network interface", + type: "pointer", + subtype: "package", + "package-id": "nostr-wallet-connect", + target: "tor-address", + interface: "main", + }, + "lan-address": { + name: "LAN Address", + description: "The LAN address of the network interface", + type: "pointer", + subtype: "package", + "package-id": "nostr-wallet-connect", + target: "lan-address", + interface: "main", + }, + "nostr-relay": { + type: "string", + name: "Nostr Relay", + default: "wss://relay.getalby.com/v1", + description: "The Nostr Relay to use for Nostr Wallet Connect connections", + copyable: true, + nullable: false, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts new file mode 100644 index 000000000..0cea482c7 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts @@ -0,0 +1,187 @@ +export default { + "tor-address": { + name: "Tor Address", + description: "The Tor address for the websocket server.", + type: "pointer", + subtype: "package", + "package-id": "nostr", + target: "tor-address", + interface: "websocket", + }, + "lan-address": { + name: "Tor Address", + description: "The LAN address for the websocket server.", + type: "pointer", + subtype: "package", + "package-id": "nostr", + target: "lan-address", + interface: "websocket", + }, + "relay-type": { + type: "union", + name: "Relay Type", + warning: + "Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.", + tag: { + id: "type", + name: "Relay Type", + description: + "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.", + "variant-names": { private: "Private", public: "Public" }, + }, + default: "private", + variants: { + private: { + pubkey_whitelist: { + name: "Pubkey Whitelist (hex)", + description: + "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.", + type: "list", + range: "[1,*)", + subtype: "string", + spec: { + placeholder: "hex (not npub) pubkey", + pattern: "[0-9a-fA-F]{64}", + "pattern-description": + "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + }, + default: [], + }, + }, + public: { + info: { + name: "Relay Info", + description: "General public info about your relay", + type: "object", + spec: { + name: { + name: "Relay Name", + description: "Your relay's human-readable identifier", + type: "string", + nullable: true, + placeholder: "Bob's Public Relay", + pattern: ".{3,32}", + "pattern-description": + "Must be at least 3 character and no more than 32 characters", + masked: false, + }, + description: { + name: "Relay Description", + description: "A more detailed description for your relay", + type: "string", + nullable: true, + placeholder: "The best relay in town", + pattern: ".{6,256}", + "pattern-description": + "Must be at least 6 character and no more than 256 characters", + masked: false, + }, + pubkey: { + name: "Admin contact pubkey (hex)", + description: + "The Nostr hex (not npub) pubkey of the relay administrator", + type: "string", + nullable: true, + placeholder: "hex (not npub) pubkey", + pattern: "[0-9a-fA-F]{64}", + "pattern-description": + "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + masked: false, + }, + contact: { + name: "Admin contact email", + description: "The email address of the relay administrator", + type: "string", + nullable: true, + pattern: "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+", + "pattern-description": "Must be a valid email address.", + masked: false, + }, + }, + }, + limits: { + name: "Limits", + description: + "Data limits to protect your relay from using too many resources", + type: "object", + spec: { + messages_per_sec: { + name: "Messages Per Second Limit", + description: + "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.", + type: "number", + nullable: false, + range: "[1,*)", + integral: true, + default: 2, + units: "messages/sec", + }, + subscriptions_per_min: { + name: "Subscriptions Per Minute Limit", + description: + "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.", + type: "number", + nullable: false, + range: "[1,*)", + integral: true, + default: 10, + units: "subscriptions", + }, + max_blocking_threads: { + name: "Max Blocking Threads", + description: + "Maximum number of blocking threads used for database connections.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "threads", + default: 16, + }, + max_event_bytes: { + name: "Max Event Size", + description: + "Limit the maximum size of an EVENT message. Set to 0 for unlimited", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + max_ws_message_bytes: { + name: "Max Websocket Message Size", + description: "Maximum WebSocket message in bytes.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + max_ws_frame_bytes: { + name: "Max Websocket Frame Size", + description: "Maximum WebSocket frame size in bytes.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + event_kind_blacklist: { + name: "Event Kind Blacklist", + description: + "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds", + type: "list", + range: "[0,*)", + subtype: "number", + spec: { integral: true, placeholder: 30023, range: "(0,100000]" }, + default: [], + }, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts new file mode 100644 index 000000000..51eb06b9a --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts @@ -0,0 +1,39 @@ +export default { + "instance-name": { + type: "string", + name: "SearXNG Instance Name", + description: + "Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.", + nullable: false, + default: "My SearXNG Engine", + placeholder: "Uncle Jim SearXNG Engine", + }, + "tor-url": { + name: "Enable Tor address as the base URL", + description: + "Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.", + type: "boolean", + default: false, + }, + "enable-metrics": { + name: "Enable Stats", + description: + "Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending `/stats` or `/stats/errors` to your SearXNG URL.", + type: "boolean", + default: true, + }, //, + // "email-address": { + // "type": "string", + // "name": "Email Address", + // "description": "Your Email address - required to create an SSL certificate.", + // "nullable": false, + // "default": "youremail@domain.com", + // }, + // "public-host": { + // "type": "string", + // "name": "Public Domain Name", + // "description": "Enter a domain name here if you want to share your SearXNG engine publicly. You will also need to modify your domain name's DNS settings to point to your Start9 server.", + // "nullable": true, + // "placeholder": "https://search.mydomain.com" + // } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts new file mode 100644 index 000000000..18b520097 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts @@ -0,0 +1,191 @@ +export default { + id: "synapse", + title: "Synapse", + version: "1.98.0", + "release-notes": + "* Upstream code update\n* Synapse Admin updated to the latest version - ([full changelog](https://github.com/Awesome-Technologies/synapse-admin/compare/0.8.7...0.9.1))\n* Instructions update\n* Updated package and upstream repositories links\n* Full list of upstream changes available [here](https://github.com/element-hq/synapse/compare/v1.95.1...v1.98.0)\n", + license: "Apache-2.0", + "wrapper-repo": "https://github.com/Start9Labs/synapse-startos", + "upstream-repo": "https://github.com/element-hq/synapse", + "support-site": "https://github.com/element-hq/synapse/issues", + "marketing-site": "https://matrix.org/", + build: ["make"], + description: { + short: + "Synapse is a battle-tested implementation of the Matrix protocol, the killer of all messaging apps.", + long: "Synapse is the battle-tested, reference implementation of the Matrix protocol. Matrix is a next-generation, federated, full-featured, encrypted, independent messaging system. There are no trusted third parties involved. (see matrix.org for details).", + }, + assets: { + license: "LICENSE", + icon: "icon.png", + instructions: "instructions.md", + }, + main: { + type: "docker", + image: "main", + entrypoint: "docker_entrypoint.sh", + args: [], + mounts: { + main: "/data", + cert: "/mnt/cert", + "admin-cert": "/mnt/admin-cert", + }, + }, + "health-checks": { + federation: { + name: "Federation", + type: "docker", + image: "main", + system: false, + entrypoint: "check-federation.sh", + args: [], + mounts: {}, + "io-format": "json", + inject: true, + }, + "synapse-admin": { + name: "Admin interface", + "success-message": + "Synapse Admin is ready to be visited in a web browser.", + type: "docker", + image: "main", + system: false, + entrypoint: "check-ui.sh", + args: [], + mounts: {}, + "io-format": "yaml", + inject: true, + }, + "user-signups-off": { + name: "User Signups Off", + type: "docker", + image: "main", + system: false, + entrypoint: "user-signups-off.sh", + args: [], + mounts: {}, + "io-format": "yaml", + inject: true, + }, + }, + config: { + get: { + type: "script", + }, + set: { + type: "script", + }, + }, + properties: { + type: "script", + }, + volumes: { + main: { + type: "data", + }, + cert: { + type: "certificate", + "interface-id": "main", + }, + "admin-cert": { + type: "certificate", + "interface-id": "admin", + }, + }, + alerts: { + start: + "After your first run, Synapse needs a little time to establish a stable TOR connection over federation. We kindly ask for your patience during this process. Remember, great things take time! 🕒", + }, + interfaces: { + main: { + name: "Homeserver Address", + description: + "Used by clients and other servers to connect with your homeserver", + "tor-config": { + "port-mapping": { + "80": "80", + "443": "443", + "8448": "8448", + }, + }, + ui: false, + protocols: ["tcp", "http", "matrix"], + }, + admin: { + name: "Admin Portal", + description: "A web application for administering your Synapse server", + "tor-config": { + "port-mapping": { + "80": "8080", + "443": "4433", + }, + }, + "lan-config": { + "443": { + ssl: true, + internal: 8080, + }, + }, + ui: true, + protocols: ["tcp", "http"], + }, + }, + dependencies: {}, + backup: { + create: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "create", "/mnt/backup", "/data"], + mounts: { + BACKUP: "/mnt/backup", + main: "/data", + }, + }, + restore: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "restore", "/mnt/backup", "/data"], + mounts: { + BACKUP: "/mnt/backup", + main: "/data", + }, + }, + }, + actions: { + "reset-first-user": { + name: "Reset First User", + description: + "This action will reset the password of the first user in your database to a random value.", + "allowed-statuses": ["stopped"], + implementation: { + type: "docker", + image: "main", + system: false, + entrypoint: "docker_entrypoint.sh", + args: ["reset-first-user"], + mounts: { + main: "/data", + }, + "io-format": "json", + }, + }, + }, + migrations: { + from: { + "*": { + type: "script", + args: ["from"], + }, + }, + to: { + "*": { + type: "script", + args: ["to"], + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap new file mode 100644 index 000000000..2c3d4b167 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -0,0 +1,1062 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = ` +{ + "advanced": { + "description": "Advanced Settings", + "name": "Advanced", + "spec": { + "blockfilters": { + "description": "Settings for storing and serving compact block filters", + "name": "Block Filters", + "spec": { + "blockfilterindex": { + "default": true, + "description": "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + "disabled": false, + "immutable": false, + "name": "Compute Compact Block Filters (BIP158)", + "type": "toggle", + "warning": null, + }, + "peerblockfilters": { + "default": false, + "description": "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + "disabled": false, + "immutable": false, + "name": "Serve Compact Block Filters to Peers (BIP157)", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "bloomfilters": { + "description": "Setting for serving Bloom Filters", + "name": "Bloom Filters (BIP37)", + "spec": { + "peerbloomfilters": { + "default": false, + "description": "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", + "disabled": false, + "immutable": false, + "name": "Serve Bloom Filters to Peers", + "type": "toggle", + "warning": "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", + }, + }, + "type": "object", + "warning": null, + }, + "dbcache": { + "default": null, + "description": "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Database Cache", + "placeholder": null, + "required": false, + "step": null, + "type": "number", + "units": "MiB", + "warning": "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + }, + "mempool": { + "description": "Mempool Settings", + "name": "Mempool", + "spec": { + "datacarrier": { + "default": true, + "description": "Relay transactions with OP_RETURN outputs", + "disabled": false, + "immutable": false, + "name": "Relay OP_RETURN Transactions", + "type": "toggle", + "warning": null, + }, + "datacarriersize": { + "default": 83, + "description": "Maximum size of data in OP_RETURN outputs to relay", + "disabled": false, + "immutable": false, + "integer": true, + "max": 10000, + "min": null, + "name": "Max OP_RETURN Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "maxmempool": { + "default": 300, + "description": "Keep the transaction memory pool below megabytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Max Mempool Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "MiB", + "warning": null, + }, + "mempoolexpiry": { + "default": 336, + "description": "Do not keep transactions in the mempool longer than hours.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Mempool Expiration", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "Hr", + "warning": null, + }, + "mempoolfullrbf": { + "default": true, + "description": "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies", + "disabled": false, + "immutable": false, + "name": "Enable Full RBF", + "type": "toggle", + "warning": null, + }, + "permitbaremultisig": { + "default": true, + "description": "Relay non-P2SH multisig transactions", + "disabled": false, + "immutable": false, + "name": "Permit Bare Multisig", + "type": "toggle", + "warning": null, + }, + "persistmempool": { + "default": true, + "description": "Save the mempool on shutdown and load on restart.", + "disabled": false, + "immutable": false, + "name": "Persist Mempool", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "peers": { + "description": "Peer Connection Settings", + "name": "Peers", + "spec": { + "addnode": { + "default": [], + "description": "Add addresses of nodes to connect to.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Add Nodes", + "spec": { + "displayAs": null, + "spec": { + "hostname": { + "default": null, + "description": "Domain or IP address of bitcoin peer", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Hostname", + "patterns": [ + { + "description": "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + "regex": "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + "port": { + "default": null, + "description": "Port that peer is listening on for inbound p2p connections", + "disabled": false, + "immutable": false, + "integer": true, + "max": 65535, + "min": null, + "name": "Port", + "placeholder": null, + "required": false, + "step": null, + "type": "number", + "units": null, + "warning": null, + }, + }, + "type": "object", + "uniqueBy": null, + }, + "type": "list", + "warning": null, + }, + "listen": { + "default": true, + "description": "Allow other nodes to find your server on the network.", + "disabled": false, + "immutable": false, + "name": "Make Public", + "type": "toggle", + "warning": null, + }, + "onlyconnect": { + "default": false, + "description": "Only connect to specified peers.", + "disabled": false, + "immutable": false, + "name": "Disable Peer Discovery", + "type": "toggle", + "warning": null, + }, + "onlyonion": { + "default": false, + "description": "Only connect to peers over Tor.", + "disabled": false, + "immutable": false, + "name": "Disable Clearnet", + "type": "toggle", + "warning": null, + }, + "v2transport": { + "default": false, + "description": "Enable or disable the use of BIP324 V2 P2P transport protocol.", + "disabled": false, + "immutable": false, + "name": "Use V2 P2P Transport Protocol", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "pruning": { + "default": "disabled", + "description": "- Disabled: Disable pruning +- Automatic: Limit blockchain size on disk to a certain number of megabytes +", + "disabled": false, + "immutable": false, + "name": "Pruning Mode", + "type": "union", + "variants": { + "automatic": { + "name": "Automatic", + "spec": { + "size": { + "default": 550, + "description": "Limit of blockchain size on disk.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 999999, + "min": 550, + "name": "Max Chain Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "MiB", + "warning": "Increasing this value will require re-syncing your node.", + }, + }, + }, + "disabled": { + "name": "Disabled", + "spec": {}, + }, + }, + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "coinstatsindex": { + "default": false, + "description": "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space", + "disabled": false, + "immutable": false, + "name": "Coinstats Index", + "type": "toggle", + "warning": null, + }, + "rpc": { + "description": "RPC configuration options.", + "name": "RPC Settings", + "spec": { + "advanced": { + "description": "Advanced RPC Settings", + "name": "Advanced", + "spec": { + "auth": { + "default": [], + "description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Authorization", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Each item must be of the form ":$".", + "regex": "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + }, + ], + "placeholder": null, + "type": "text", + }, + "type": "list", + "warning": null, + }, + "servertimeout": { + "default": 30, + "description": "Number of seconds after which an uncompleted RPC call will time out.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 300, + "min": 5, + "name": "Rpc Server Timeout", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "seconds", + "warning": null, + }, + "threads": { + "default": 16, + "description": "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 64, + "min": 1, + "name": "Threads", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": null, + "warning": null, + }, + "workqueue": { + "default": 128, + "description": "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 256, + "min": 8, + "name": "Work Queue", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "requests", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "enable": { + "default": true, + "description": "Allow remote RPC requests.", + "disabled": false, + "immutable": false, + "name": "Enable", + "type": "toggle", + "warning": null, + }, + "password": { + "default": { + "charset": "a-z,2-7", + "len": 20, + }, + "description": "The password for connecting to Bitcoin over RPC.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": true, + "maxLength": null, + "minLength": null, + "name": "RPC Password", + "patterns": [ + { + "description": "Must be alphanumeric (can contain underscore).", + "regex": "^[a-zA-Z0-9_]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": "You will need to restart all services that depend on Bitcoin.", + }, + "username": { + "default": "bitcoin", + "description": "The username for connecting to Bitcoin over RPC.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": true, + "maxLength": null, + "minLength": null, + "name": "Username", + "patterns": [ + { + "description": "Must be alphanumeric (can contain underscore).", + "regex": "^[a-zA-Z0-9_]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": "You will need to restart all services that depend on Bitcoin.", + }, + }, + "type": "object", + "warning": null, + }, + "txindex": { + "default": true, + "description": "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like \`gettransaction\`.", + "disabled": false, + "immutable": false, + "name": "Transaction Index", + "type": "toggle", + "warning": null, + }, + "wallet": { + "description": "Wallet Settings", + "name": "Wallet", + "spec": { + "avoidpartialspends": { + "default": true, + "description": "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + "disabled": false, + "immutable": false, + "name": "Avoid Partial Spends", + "type": "toggle", + "warning": null, + }, + "discardfee": { + "default": 0.0001, + "description": "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + "disabled": false, + "immutable": false, + "integer": false, + "max": 0.01, + "min": null, + "name": "Discard Change Tolerance", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "BTC/kB", + "warning": null, + }, + "enable": { + "default": true, + "description": "Load the wallet and enable wallet RPC calls.", + "disabled": false, + "immutable": false, + "name": "Enable Wallet", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "zmq-enabled": { + "default": true, + "description": "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", + "disabled": false, + "immutable": false, + "name": "ZeroMQ Enabled", + "type": "toggle", + "warning": null, + }, +} +`; + +exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` +{ + "homepage": { + "default": "welcome", + "description": null, + "disabled": false, + "immutable": false, + "name": "Type", + "type": "union", + "variants": { + "index": { + "name": "Table of Contents", + "spec": {}, + }, + "redirect": { + "name": "Redirect", + "spec": { + "target": { + "default": null, + "description": "The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Target Subdomain", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens.", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + }, + }, + "web-page": { + "name": "Web Page", + "spec": { + "folder": { + "default": null, + "description": "The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Folder Path", + "patterns": [ + { + "description": "Must be a valid relative file path", + "regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + }, + ], + "placeholder": "e.g. websites/resume", + "required": true, + "type": "text", + "warning": null, + }, + "source": { + "default": "nextcloud", + "description": "The service that contains your website files.", + "disabled": false, + "immutable": false, + "name": "Folder Location", + "type": "select", + "values": { + "filebrowser": "filebrowser", + "nextcloud": "nextcloud", + }, + "warning": null, + }, + }, + }, + "welcome": { + "name": "Welcome", + "spec": {}, + }, + }, + "warning": null, + }, + "subdomains": { + "default": [], + "description": "The websites you want to serve.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Subdomains", + "spec": { + "displayAs": "{{name}}", + "spec": { + "name": { + "default": null, + "description": "The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Subdomain name", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + "settings": { + "default": "web-page", + "description": null, + "disabled": false, + "immutable": false, + "name": "Type", + "type": "union", + "variants": { + "redirect": { + "name": "Redirect", + "spec": { + "target": { + "default": null, + "description": "The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Target Subdomain", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens.", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + }, + }, + "web-page": { + "name": "Web Page", + "spec": { + "folder": { + "default": null, + "description": "The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Folder Path", + "patterns": [ + { + "description": "Must be a valid relative file path", + "regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + }, + ], + "placeholder": "e.g. websites/resume", + "required": true, + "type": "text", + "warning": null, + }, + "source": { + "default": "nextcloud", + "description": "The service that contains your website files.", + "disabled": false, + "immutable": false, + "name": "Folder Location", + "type": "select", + "values": { + "filebrowser": "filebrowser", + "nextcloud": "nextcloud", + }, + "warning": null, + }, + }, + }, + }, + "warning": null, + }, + }, + "type": "object", + "uniqueBy": "name", + }, + "type": "list", + "warning": null, + }, +} +`; + +exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = ` +{ + "nostr-relay": { + "default": "wss://relay.getalby.com/v1", + "description": "The Nostr Relay to use for Nostr Wallet Connect connections", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Nostr Relay", + "patterns": [], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, +} +`; + +exports[`transformConfigSpec transformConfigSpec(nostr2) 1`] = ` +{ + "relay-type": { + "default": "private", + "description": "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.", + "disabled": false, + "immutable": false, + "name": "Relay Type", + "type": "union", + "variants": { + "private": { + "name": "Private", + "spec": { + "pubkey_whitelist": { + "default": [], + "description": "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.", + "disabled": false, + "maxLength": null, + "minLength": 1, + "name": "Pubkey Whitelist (hex)", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + "regex": "[0-9a-fA-F]{64}", + }, + ], + "placeholder": "hex (not npub) pubkey", + "type": "text", + }, + "type": "list", + "warning": null, + }, + }, + }, + "public": { + "name": "Public", + "spec": { + "info": { + "description": "General public info about your relay", + "name": "Relay Info", + "spec": { + "contact": { + "default": null, + "description": "The email address of the relay administrator", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Admin contact email", + "patterns": [ + { + "description": "Must be a valid email address.", + "regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+", + }, + ], + "placeholder": null, + "required": false, + "type": "text", + "warning": null, + }, + "description": { + "default": null, + "description": "A more detailed description for your relay", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Relay Description", + "patterns": [ + { + "description": "Must be at least 6 character and no more than 256 characters", + "regex": ".{6,256}", + }, + ], + "placeholder": "The best relay in town", + "required": false, + "type": "text", + "warning": null, + }, + "name": { + "default": null, + "description": "Your relay's human-readable identifier", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Relay Name", + "patterns": [ + { + "description": "Must be at least 3 character and no more than 32 characters", + "regex": ".{3,32}", + }, + ], + "placeholder": "Bob's Public Relay", + "required": false, + "type": "text", + "warning": null, + }, + "pubkey": { + "default": null, + "description": "The Nostr hex (not npub) pubkey of the relay administrator", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Admin contact pubkey (hex)", + "patterns": [ + { + "description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + "regex": "[0-9a-fA-F]{64}", + }, + ], + "placeholder": "hex (not npub) pubkey", + "required": false, + "type": "text", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "limits": { + "description": "Data limits to protect your relay from using too many resources", + "name": "Limits", + "spec": { + "event_kind_blacklist": { + "default": [], + "description": "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Event Kind Blacklist", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Integral number type", + "regex": "[0-9]+", + }, + ], + "placeholder": "30023", + "type": "text", + }, + "type": "list", + "warning": null, + }, + "max_blocking_threads": { + "default": 16, + "description": "Maximum number of blocking threads used for database connections.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Blocking Threads", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "threads", + "warning": null, + }, + "max_event_bytes": { + "default": 131072, + "description": "Limit the maximum size of an EVENT message. Set to 0 for unlimited", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Event Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "max_ws_frame_bytes": { + "default": 131072, + "description": "Maximum WebSocket frame size in bytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Websocket Frame Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "max_ws_message_bytes": { + "default": 131072, + "description": "Maximum WebSocket message in bytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Websocket Message Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "messages_per_sec": { + "default": 2, + "description": "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Messages Per Second Limit", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "messages/sec", + "warning": null, + }, + "subscriptions_per_min": { + "default": 10, + "description": "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Subscriptions Per Minute Limit", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "subscriptions", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + }, + }, + }, + "warning": null, + }, +} +`; + +exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = ` +{ + "enable-metrics": { + "default": true, + "description": "Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending \`/stats\` or \`/stats/errors\` to your SearXNG URL.", + "disabled": false, + "immutable": false, + "name": "Enable Stats", + "type": "toggle", + "warning": null, + }, + "instance-name": { + "default": "My SearXNG Engine", + "description": "Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "SearXNG Instance Name", + "patterns": [], + "placeholder": "Uncle Jim SearXNG Engine", + "required": true, + "type": "text", + "warning": null, + }, + "tor-url": { + "default": false, + "description": "Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.", + "disabled": false, + "immutable": false, + "name": "Enable Tor address as the base URL", + "type": "toggle", + "warning": null, + }, +} +`; diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts new file mode 100644 index 000000000..2b32afd85 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -0,0 +1,1185 @@ +import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk" +import * as fs from "fs/promises" + +import { polyfillEffects } from "./polyfillEffects" +import { fromDuration } from "../../../Models/Duration" +import { System } from "../../../Interfaces/System" +import { matchManifest, Manifest } from "./matchManifest" +import * as childProcess from "node:child_process" +import { DockerProcedureContainer } from "./DockerProcedureContainer" +import { promisify } from "node:util" +import * as U from "./oldEmbassyTypes" +import { MainLoop } from "./MainLoop" +import { + matches, + boolean, + dictionary, + literal, + literals, + object, + string, + unknown, + any, + tuple, + number, + anyOf, + deferred, + Parser, + array, +} from "ts-matches" +import { AddSslOptions } from "@start9labs/start-sdk/base/lib/osBindings" +import { + BindOptionsByProtocol, + MultiHost, +} from "@start9labs/start-sdk/base/lib/interfaces/Host" +import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/base/lib/interfaces/ServiceInterfaceBuilder" +import { Effects } from "../../../Models/Effects" +import { + OldConfigSpec, + matchOldConfigSpec, + transformConfigSpec, + transformNewConfigToOld, + transformOldConfigToNew, +} from "./transformConfigSpec" +import { partialDiff } from "@start9labs/start-sdk/base/lib/util" + +type Optional = A | undefined | null +function todo(): never { + throw new Error("Not implemented") +} + +const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" +export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" +const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath +const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath + +const matchResult = object({ + result: any, +}) +const matchError = object({ + error: string, +}) +const matchErrorCode = object<{ + "error-code": [number, string] | readonly [number, string] +}>({ + "error-code": tuple(number, string), +}) + +const assertNever = ( + x: never, + message = "Not expecting to get here: ", +): never => { + throw new Error(message + JSON.stringify(x)) +} +/** + Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one. +*/ +const fromReturnType = (a: U.ResultType): A => { + if (matchResult.test(a)) { + return a.result + } + if (matchError.test(a)) { + console.info({ passedErrorStack: new Error().stack, error: a.error }) + throw { error: a.error } + } + if (matchErrorCode.test(a)) { + const [code, message] = a["error-code"] + throw { error: message, code } + } + return assertNever(a) +} + +const matchSetResult = object( + { + "depends-on": dictionary([string, array(string)]), + dependsOn: dictionary([string, array(string)]), + signal: literals( + "SIGTERM", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGKILL", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGPIPE", + "SIGALRM", + "SIGSTKFLT", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGTSTP", + "SIGTTIN", + "SIGTTOU", + "SIGURG", + "SIGXCPU", + "SIGXFSZ", + "SIGVTALRM", + "SIGPROF", + "SIGWINCH", + "SIGIO", + "SIGPWR", + "SIGSYS", + "SIGINFO", + ), + }, + ["depends-on", "dependsOn"], +) + +type OldGetConfigRes = { + config?: null | Record + spec: OldConfigSpec +} + +export type PropertiesValue = + | { + /** The type of this value, either "string" or "object" */ + type: "object" + /** A nested mapping of values. The user will experience this as a nested page with back button */ + value: { [k: string]: PropertiesValue } + /** (optional) A human readable description of the new set of values */ + description: string | null + } + | { + /** The type of this value, either "string" or "object" */ + type: "string" + /** The value to display to the user */ + value: string + /** A human readable description of the value */ + description: string | null + /** Whether or not to mask the value, for example, when displaying a password */ + masked: boolean | null + /** Whether or not to include a button for copying the value to clipboard */ + copyable: boolean | null + /** Whether or not to include a button for displaying the value as a QR code */ + qr: boolean | null + } + +export type PropertiesReturn = { + [key: string]: PropertiesValue +} + +export type PackagePropertiesV2 = { + [name: string]: PackagePropertyObject | PackagePropertyString +} +export type PackagePropertyString = { + type: "string" + description?: string + value: string + /** Let's the ui make this copyable button */ + copyable?: boolean + /** Let the ui create a qr for this field */ + qr?: boolean + /** Hiding the value unless toggled off for field */ + masked?: boolean +} +export type PackagePropertyObject = { + value: PackagePropertiesV2 + type: "object" + description: string +} + +const asProperty_ = ( + x: PackagePropertyString | PackagePropertyObject, +): PropertiesValue => { + if (x.type === "object") { + return { + ...x, + value: Object.fromEntries( + Object.entries(x.value).map(([key, value]) => [ + key, + asProperty_(value), + ]), + ), + } + } + return { + masked: false, + description: null, + qr: null, + copyable: null, + ...x, + } +} +const asProperty = (x: PackagePropertiesV2): PropertiesReturn => + Object.fromEntries( + Object.entries(x).map(([key, value]) => [key, asProperty_(value)]), + ) +const [matchPackageProperties, setMatchPackageProperties] = + deferred() +const matchPackagePropertyObject: Parser = + object({ + value: matchPackageProperties, + type: literal("object"), + description: string, + }) + +const matchPackagePropertyString: Parser = + object( + { + type: literal("string"), + description: string, + value: string, + copyable: boolean, + qr: boolean, + masked: boolean, + }, + ["copyable", "description", "qr", "masked"], + ) +setMatchPackageProperties( + dictionary([ + string, + anyOf(matchPackagePropertyObject, matchPackagePropertyString), + ]), +) + +const matchProperties = object({ + version: literal(2), + data: matchPackageProperties, +}) + +function convertProperties( + name: string, + value: PropertiesValue, +): T.ActionResultMember { + if (value.type === "string") { + return { + type: "single", + name, + description: value.description, + copyable: value.copyable || false, + masked: value.masked || false, + qr: value.qr || false, + value: value.value, + } + } + return { + type: "group", + name, + description: value.description, + value: Object.entries(value.value).map(([name, value]) => + convertProperties(name, value), + ), + } +} + +const DEFAULT_REGISTRY = "https://registry.start9.com" +export class SystemForEmbassy implements System { + currentRunning: MainLoop | undefined + static async of(manifestLocation: string = MANIFEST_LOCATION) { + const moduleCode = await import(EMBASSY_JS_LOCATION) + .catch((_) => require(EMBASSY_JS_LOCATION)) + .catch(async (_) => { + console.error(utils.asError("Could not load the js")) + console.error({ + exists: await fs.stat(EMBASSY_JS_LOCATION), + }) + return {} + }) + const manifestData = await fs.readFile(manifestLocation, "utf-8") + return new SystemForEmbassy( + matchManifest.unsafeCast(JSON.parse(manifestData)), + moduleCode, + ) + } + + constructor( + readonly manifest: Manifest, + readonly moduleCode: Partial, + ) {} + + async containerInit(effects: Effects): Promise { + for (let depId in this.manifest.dependencies) { + if (this.manifest.dependencies[depId].config) { + await this.dependenciesAutoconfig(effects, depId, null) + } + } + await effects.setMainStatus({ status: "stopped" }) + await this.exportActions(effects) + await this.exportNetwork(effects) + await this.containerSetDependencies(effects) + } + async containerSetDependencies(effects: T.Effects) { + const oldDeps: Record = Object.fromEntries( + await effects + .getDependencies() + .then((x) => + x.flatMap((x) => + x.kind === "running" ? [[x.id, x?.healthChecks || []]] : [], + ), + ) + .catch(() => []), + ) + await this.setDependencies(effects, oldDeps, false) + } + + async exit(): Promise { + if (this.currentRunning) await this.currentRunning.clean() + delete this.currentRunning + } + + async start(effects: T.Effects): Promise { + effects.constRetry = utils.once(() => effects.restart()) + if (!!this.currentRunning) return + + this.currentRunning = await MainLoop.of(this, effects) + } + callCallback(_callback: number, _args: any[]): void {} + async stop(): Promise { + const { currentRunning } = this + this.currentRunning?.clean() + delete this.currentRunning + if (currentRunning) { + await currentRunning.clean({ + timeout: fromDuration(this.manifest.main["sigterm-timeout"] || "30s"), + }) + } + } + + async packageInit(effects: Effects, timeoutMs: number | null): Promise { + const previousVersion = await effects.getDataVersion() + if (previousVersion) { + if ( + (await this.migration(effects, previousVersion, timeoutMs)).configured + ) { + await effects.action.clearRequests({ only: ["needs-config"] }) + } + await effects.setDataVersion({ + version: ExtendedVersion.parseEmver(this.manifest.version).toString(), + }) + } else if (this.manifest.config) { + await effects.action.request({ + packageId: this.manifest.id, + actionId: "config", + severity: "critical", + replayId: "needs-config", + reason: "This service must be configured before it can be run", + }) + } + } + async exportNetwork(effects: Effects) { + for (const [id, interfaceValue] of Object.entries( + this.manifest.interfaces, + )) { + const host = new MultiHost({ effects, id }) + const internalPorts = new Set( + Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {}) + .map(Number.parseInt) + .concat( + ...Object.values(interfaceValue["lan-config"] ?? {}).map( + (c) => c.internal, + ), + ) + .filter(Boolean), + ) + const bindings = Array.from(internalPorts).map< + [number, BindOptionsByProtocol] + >((port) => { + const lanPort = Object.entries(interfaceValue["lan-config"] ?? {}).find( + ([external, internal]) => internal.internal === port, + )?.[0] + const torPort = Object.entries( + interfaceValue["tor-config"]?.["port-mapping"] ?? {}, + ).find( + ([external, internal]) => Number.parseInt(internal) === port, + )?.[0] + let addSsl: AddSslOptions | null = null + if (lanPort) { + const lanPortNum = Number.parseInt(lanPort) + if (lanPortNum === 443) { + return [port, { protocol: "http", preferredExternalPort: 80 }] + } + addSsl = { + preferredExternalPort: lanPortNum, + alpn: { specified: [] }, + } + } + return [ + port, + { + protocol: null, + secure: null, + preferredExternalPort: Number.parseInt( + torPort || lanPort || String(port), + ), + addSsl, + }, + ] + }) + + await Promise.all( + bindings.map(async ([internal, options]) => { + if (internal == null) { + return + } + if (options?.preferredExternalPort == null) { + return + } + const origin = await host.bindPort(internal, options) + await origin.export([ + new ServiceInterfaceBuilder({ + effects, + name: interfaceValue.name, + id: `${id}-${internal}`, + description: interfaceValue.description, + type: + interfaceValue.ui && + (origin.scheme === "http" || origin.sslScheme === "https") + ? "ui" + : "api", + masked: false, + path: "", + schemeOverride: null, + search: {}, + username: null, + }), + ]) + }), + ) + } + } + async getActionInput( + effects: Effects, + actionId: string, + timeoutMs: number | null, + ): Promise { + if (actionId === "config") { + const config = await this.getConfig(effects, timeoutMs) + return { spec: config.spec, value: config.config } + } else if (actionId === "properties") { + return null + } else { + const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"] + if (!oldSpec) return null + return { + spec: transformConfigSpec(oldSpec as OldConfigSpec), + value: null, + } + } + } + async runAction( + effects: Effects, + actionId: string, + input: unknown, + timeoutMs: number | null, + ): Promise { + if (actionId === "config") { + await this.setConfig(effects, input, timeoutMs) + return null + } else if (actionId === "properties") { + return { + version: "1", + title: "Properties", + message: null, + result: { + type: "group", + value: Object.entries(await this.properties(effects, timeoutMs)).map( + ([name, value]) => convertProperties(name, value), + ), + }, + } + } else { + return this.action(effects, actionId, input, timeoutMs) + } + } + async exportActions(effects: Effects) { + const manifest = this.manifest + const actions = { + ...manifest.actions, + } + if (manifest.config) { + actions.config = { + name: "Configure", + description: `Customize ${manifest.title}`, + "allowed-statuses": ["running", "stopped"], + "input-spec": {}, + implementation: { type: "script", args: [] }, + } + } + if (manifest.properties) { + actions.properties = { + name: "Properties", + description: + "Runtime information, credentials, and other values of interest", + "allowed-statuses": ["running", "stopped"], + "input-spec": null, + implementation: { type: "script", args: [] }, + } + } + for (const [actionId, action] of Object.entries(actions)) { + const hasRunning = !!action["allowed-statuses"].find( + (x) => x === "running", + ) + const hasStopped = !!action["allowed-statuses"].find( + (x) => x === "stopped", + ) + // prettier-ignore + const allowedStatuses = hasRunning && hasStopped ? "any": + hasRunning ? "only-running" : + "only-stopped" + await effects.action.export({ + id: actionId, + metadata: { + name: action.name, + description: action.description, + warning: action.warning || null, + visibility: "enabled", + allowedStatuses, + hasInput: !!action["input-spec"], + group: null, + }, + }) + } + await effects.action.clear({ except: Object.keys(actions) }) + } + async packageUninit( + effects: Effects, + nextVersion: Optional, + timeoutMs: number | null, + ): Promise { + // TODO Do a migration down if the version exists + await effects.setMainStatus({ status: "stopped" }) + } + + async createBackup( + effects: Effects, + timeoutMs: number | null, + ): Promise { + const backup = this.manifest.backup.create + if (backup.type === "docker") { + const commands = [backup.entrypoint, ...backup.args] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + backup, + { + ...this.manifest.volumes, + BACKUP: { type: "backup", readonly: false }, + }, + `Backup - ${commands.join(" ")}`, + ) + await container.execFail(commands, timeoutMs) + } else { + const moduleCode = await this.moduleCode + await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) + } + } + async restoreBackup( + effects: Effects, + timeoutMs: number | null, + ): Promise { + const restoreBackup = this.manifest.backup.restore + if (restoreBackup.type === "docker") { + const commands = [restoreBackup.entrypoint, ...restoreBackup.args] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + restoreBackup, + { + ...this.manifest.volumes, + BACKUP: { type: "backup", readonly: true }, + }, + `Restore Backup - ${commands.join(" ")}`, + ) + await container.execFail(commands, timeoutMs) + } else { + const moduleCode = await this.moduleCode + await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest)) + } + } + async getConfig(effects: Effects, timeoutMs: number | null) { + return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig) + } + private async getConfigUncleaned( + effects: Effects, + timeoutMs: number | null, + ): Promise { + const config = this.manifest.config?.get + if (!config) return { spec: {} } + if (config.type === "docker") { + const commands = [config.entrypoint, ...config.args] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + config, + this.manifest.volumes, + `Get Config - ${commands.join(" ")}`, + ) + // TODO: yaml + return JSON.parse( + (await container.execFail(commands, timeoutMs)).stdout.toString(), + ) + } else { + const moduleCode = await this.moduleCode + const method = moduleCode.getConfig + if (!method) throw new Error("Expecting that the method getConfig exists") + return (await method(polyfillEffects(effects, this.manifest)).then( + (x) => { + if ("result" in x) return JSON.parse(JSON.stringify(x.result)) + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + }, + )) as any + } + } + async setConfig( + effects: Effects, + newConfigWithoutPointers: unknown, + timeoutMs: number | null, + ): Promise { + const spec = await this.getConfigUncleaned(effects, timeoutMs).then( + (x) => x.spec, + ) + const newConfig = transformNewConfigToOld( + spec, + structuredClone(newConfigWithoutPointers as Record), + ) + await updateConfig(effects, this.manifest, spec, newConfig) + await effects.store.set({ + path: EMBASSY_POINTER_PATH_PREFIX, + value: newConfig, + }) + const setConfigValue = this.manifest.config?.set + if (!setConfigValue) return + if (setConfigValue.type === "docker") { + const commands = [ + setConfigValue.entrypoint, + ...setConfigValue.args, + JSON.stringify(newConfig), + ] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + setConfigValue, + this.manifest.volumes, + `Set Config - ${commands.join(" ")}`, + ) + const answer = matchSetResult.unsafeCast( + JSON.parse( + (await container.execFail(commands, timeoutMs)).stdout.toString(), + ), + ) + const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} + await this.setDependencies(effects, dependsOn, true) + return + } else if (setConfigValue.type === "script") { + const moduleCode = await this.moduleCode + const method = moduleCode.setConfig + if (!method) throw new Error("Expecting that the method setConfig exists") + + const answer = matchSetResult.unsafeCast( + await method( + polyfillEffects(effects, this.manifest), + newConfig as U.Config, + ).then((x): T.SetResult => { + if ("result" in x) + return { + dependsOn: x.result["depends-on"], + signal: + x.result.signal === "SIGEMT" ? "SIGTERM" : x.result.signal, + } + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + }), + ) + const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} + await this.setDependencies(effects, dependsOn, true) + return + } + } + private async setDependencies( + effects: Effects, + rawDepends: { [x: string]: readonly string[] }, + configuring: boolean, + ) { + const storedDependsOn = (await effects.store.get({ + packageId: this.manifest.id, + path: EMBASSY_DEPENDS_ON_PATH_PREFIX, + })) as Record + + const requiredDeps = { + ...Object.fromEntries( + Object.entries(this.manifest.dependencies || {}) + ?.filter((x) => x[1].requirement.type === "required") + .map((x) => [x[0], []]) || [], + ), + } + + const dependsOn: Record = configuring + ? { + ...requiredDeps, + ...rawDepends, + } + : storedDependsOn + ? storedDependsOn + : requiredDeps + + await effects.store.set({ + path: EMBASSY_DEPENDS_ON_PATH_PREFIX, + value: dependsOn, + }) + + await effects.setDependencies({ + dependencies: Object.entries(dependsOn).flatMap( + ([key, value]): T.Dependencies => { + const dependency = this.manifest.dependencies?.[key] + if (!dependency) return [] + const versionRange = dependency.version + const kind = "running" + return [ + { + id: key, + versionRange, + kind, + healthChecks: [...value], + }, + ] + }, + ), + }) + } + + async migration( + effects: Effects, + fromVersion: string, + timeoutMs: number | null, + ): Promise<{ configured: boolean }> { + const fromEmver = ExtendedVersion.parseEmver(fromVersion) + const currentEmver = ExtendedVersion.parseEmver(this.manifest.version) + if (!this.manifest.migrations) return { configured: true } + const fromMigration = Object.entries(this.manifest.migrations.from) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) + .find( + ([versionEmver, procedure]) => + versionEmver.greaterThan(fromEmver) && + versionEmver.lessThanOrEqual(currentEmver), + ) + const toMigration = Object.entries(this.manifest.migrations.to) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) + .find( + ([versionEmver, procedure]) => + versionEmver.greaterThan(fromEmver) && + versionEmver.lessThanOrEqual(currentEmver), + ) + + // prettier-ignore + const migration = ( + fromEmver.greaterThan(currentEmver) ? [toMigration, fromMigration] : + [fromMigration, toMigration]).filter(Boolean)[0] + + if (migration) { + const [version, procedure] = migration + if (procedure.type === "docker") { + const commands = [ + procedure.entrypoint, + ...procedure.args, + JSON.stringify(fromVersion), + ] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + procedure, + this.manifest.volumes, + `Migration - ${commands.join(" ")}`, + ) + return JSON.parse( + (await container.execFail(commands, timeoutMs)).stdout.toString(), + ) + } else if (procedure.type === "script") { + const moduleCode = await this.moduleCode + const method = moduleCode.migration + if (!method) + throw new Error("Expecting that the method migration exists") + return (await method( + polyfillEffects(effects, this.manifest), + fromVersion as string, + ).then((x) => { + if ("result" in x) return x.result + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + })) as any + } + } + return { configured: true } + } + async properties( + effects: Effects, + timeoutMs: number | null, + ): Promise { + // TODO BLU-J set the properties ever so often + const setConfigValue = this.manifest.properties + if (!setConfigValue) throw new Error("There is no properties") + if (setConfigValue.type === "docker") { + const commands = [setConfigValue.entrypoint, ...setConfigValue.args] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + setConfigValue, + this.manifest.volumes, + `Properties - ${commands.join(" ")}`, + ) + const properties = matchProperties.unsafeCast( + JSON.parse( + (await container.execFail(commands, timeoutMs)).stdout.toString(), + ), + ) + return asProperty(properties.data) + } else if (setConfigValue.type === "script") { + const moduleCode = this.moduleCode + const method = moduleCode.properties + if (!method) + throw new Error("Expecting that the method properties exists") + const properties = matchProperties.unsafeCast( + await method(polyfillEffects(effects, this.manifest)).then( + fromReturnType, + ), + ) + return asProperty(properties.data) + } + throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`) + } + async action( + effects: Effects, + actionId: string, + formData: unknown, + timeoutMs: number | null, + ): Promise { + const actionProcedure = this.manifest.actions?.[actionId]?.implementation + const toActionResult = ({ + message, + value, + copyable, + qr, + }: U.ActionResult): T.ActionResult => ({ + version: "0", + message, + value: value ?? null, + copyable, + qr, + }) + if (!actionProcedure) throw Error("Action not found") + if (actionProcedure.type === "docker") { + const subcontainer = actionProcedure.inject + ? this.currentRunning?.mainSubContainerHandle + : undefined + + const env: Record = actionProcedure.inject + ? { + HOME: "/root", + } + : {} + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + actionProcedure, + this.manifest.volumes, + `Action ${actionId}`, + { + subcontainer, + }, + ) + return toActionResult( + JSON.parse( + ( + await container.execFail( + [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(formData), + ], + timeoutMs, + { env }, + ) + ).stdout.toString(), + ), + ) + } else { + const moduleCode = await this.moduleCode + const method = moduleCode.action?.[actionId] + if (!method) throw new Error("Expecting that the method action exists") + return await method( + polyfillEffects(effects, this.manifest), + formData as any, + ) + .then(fromReturnType) + .then(toActionResult) + } + } + async dependenciesCheck( + effects: Effects, + id: string, + oldConfig: unknown, + timeoutMs: number | null, + ): Promise { + const actionProcedure = this.manifest.dependencies?.[id]?.config?.check + if (!actionProcedure) return { message: "Action not found", value: null } + if (actionProcedure.type === "docker") { + const commands = [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(oldConfig), + ] + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + actionProcedure, + this.manifest.volumes, + `Dependencies Check - ${commands.join(" ")}`, + ) + return JSON.parse( + (await container.execFail(commands, timeoutMs)).stdout.toString(), + ) + } else if (actionProcedure.type === "script") { + const moduleCode = await this.moduleCode + const method = moduleCode.dependencies?.[id]?.check + if (!method) + throw new Error( + `Expecting that the method dependency check ${id} exists`, + ) + return (await method( + polyfillEffects(effects, this.manifest), + oldConfig as any, + ).then((x) => { + if ("result" in x) return x.result + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + })) as any + } else { + return {} + } + } + async dependenciesAutoconfig( + effects: Effects, + id: string, + timeoutMs: number | null, + ): Promise { + // TODO: docker + const oldConfig = (await effects.store.get({ + packageId: id, + path: EMBASSY_POINTER_PATH_PREFIX, + callback: () => { + this.dependenciesAutoconfig(effects, id, timeoutMs) + }, + })) as U.Config + if (!oldConfig) return + const moduleCode = await this.moduleCode + const method = moduleCode.dependencies?.[id]?.autoConfigure + if (!method) return + const newConfig = (await method( + polyfillEffects(effects, this.manifest), + JSON.parse(JSON.stringify(oldConfig)), + ).then((x) => { + if ("result" in x) return x.result + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + })) as any + const diff = partialDiff(oldConfig, newConfig) + if (diff) { + await effects.action.request({ + actionId: "config", + packageId: id, + replayId: `${id}/config`, + severity: "important", + reason: `Configure this dependency for the needs of ${this.manifest.title}`, + input: { + kind: "partial", + value: diff.diff, + }, + when: { + condition: "input-not-matches", + once: false, + }, + }) + } + } +} + +const matchPointer = object({ + type: literal("pointer"), +}) + +const matchPointerPackage = object({ + subtype: literal("package"), + target: literals("tor-key", "tor-address", "lan-address"), + "package-id": string, + interface: string, +}) +const matchPointerConfig = object({ + subtype: literal("package"), + target: literals("config"), + "package-id": string, + selector: string, + multi: boolean, +}) +const matchSpec = object({ + spec: object, +}) +const matchVariants = object({ variants: dictionary([string, unknown]) }) +function cleanSpecOfPointers(mutSpec: T): T { + if (!object.test(mutSpec)) return mutSpec + for (const key in mutSpec) { + const value = mutSpec[key] + if (matchSpec.test(value)) value.spec = cleanSpecOfPointers(value.spec) + if (matchVariants.test(value)) + value.variants = Object.fromEntries( + Object.entries(value.variants).map(([key, value]) => [ + key, + cleanSpecOfPointers(value), + ]), + ) + if (!matchPointer.test(value)) continue + delete mutSpec[key] + // // if (value.target === ) + } + + return mutSpec +} +function isKeyOf( + key: string, + ofObject: O, +): key is keyof O & string { + return key in ofObject +} + +// prettier-ignore +type CleanConfigFromPointers = + [C, S] extends [object, object] ? { + [K in (keyof C & keyof S ) & string]: ( + S[K] extends {type: "pointer"} ? never : + S[K] extends {spec: object & infer B} ? CleanConfigFromPointers : + C[K] + ) + } : + null + +async function updateConfig( + effects: Effects, + manifest: Manifest, + spec: OldConfigSpec, + mutConfigValue: Record, +) { + for (const key in spec) { + const specValue = spec[key] + + if (specValue.type === "object") { + await updateConfig( + effects, + manifest, + specValue.spec as OldConfigSpec, + mutConfigValue[key] as Record, + ) + } else if (specValue.type === "list" && specValue.subtype === "object") { + const list = mutConfigValue[key] as unknown[] + for (let val of list) { + await updateConfig( + effects, + manifest, + { ...(specValue.spec as any), type: "object" as const }, + val as Record, + ) + } + } else if (specValue.type === "union") { + const union = mutConfigValue[key] as Record + await updateConfig( + effects, + manifest, + specValue.variants[union[specValue.tag.id] as string] as OldConfigSpec, + mutConfigValue[key] as Record, + ) + } else if ( + specValue.type === "pointer" && + specValue.subtype === "package" + ) { + if (specValue.target === "config") { + const jp = require("jsonpath") + const remoteConfig = await effects.store.get({ + packageId: specValue["package-id"], + callback: () => effects.restart(), + path: EMBASSY_POINTER_PATH_PREFIX, + }) + console.debug(remoteConfig) + const configValue = specValue.multi + ? jp.query(remoteConfig, specValue.selector) + : jp.query(remoteConfig, specValue.selector, 1)[0] + mutConfigValue[key] = configValue === undefined ? null : configValue + } else if (specValue.target === "tor-key") { + throw new Error("This service uses an unsupported target TorKey") + } else { + const specInterface = specValue.interface + const serviceInterfaceId = extractServiceInterfaceId( + manifest, + specInterface, + ) + if (!serviceInterfaceId) { + mutConfigValue[key] = "" + return + } + const filled = await utils + .getServiceInterface(effects, { + packageId: specValue["package-id"], + id: serviceInterfaceId, + }) + .once() + .catch((x) => { + console.error( + "Could not get the service interface", + utils.asError(x), + ) + return null + }) + const catchFn = (fn: () => X) => { + try { + return fn() + } catch (e) { + return undefined + } + } + const url: string = + filled === null || filled.addressInfo === null + ? "" + : catchFn(() => + utils.hostnameInfoToAddress( + specValue.target === "lan-address" + ? filled.addressInfo!.localHostnames[0] || + filled.addressInfo!.onionHostnames[0] + : filled.addressInfo!.onionHostnames[0] || + filled.addressInfo!.localHostnames[0], + ), + ) || "" + mutConfigValue[key] = url + } + } + } +} +function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { + const internalPort = + Object.entries( + manifest.interfaces[specInterface]?.["lan-config"] || {}, + )[0]?.[1]?.internal || + Object.entries( + manifest.interfaces[specInterface]?.["tor-config"]?.["port-mapping"] || + {}, + )?.[0]?.[1] + + if (!internalPort) return null + const serviceInterfaceId = `${specInterface}-${internalPort}` + return serviceInterfaceId +} +async function convertToNewConfig(value: OldGetConfigRes) { + const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec) + const spec = transformConfigSpec(valueSpec) + if (!value.config) return { spec, config: null } + const config = transformOldConfigToNew(valueSpec, value.config) + return { spec, config } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts new file mode 100644 index 000000000..3730dd3b6 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts @@ -0,0 +1,12 @@ +import { matchManifest } from "./matchManifest" +import giteaManifest from "./__fixtures__/giteaManifest" +import synapseManifest from "./__fixtures__/synapseManifest" + +describe("matchManifest", () => { + test("gittea", () => { + matchManifest.unsafeCast(giteaManifest) + }) + test("synapse", () => { + matchManifest.unsafeCast(synapseManifest) + }) +}) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts new file mode 100644 index 000000000..5bda20de0 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -0,0 +1,136 @@ +import { + object, + literal, + string, + array, + boolean, + dictionary, + literals, + number, + unknown, + some, + every, +} from "ts-matches" +import { matchVolume } from "./matchVolume" +import { matchDockerProcedure } from "../../../Models/DockerProcedure" + +const matchJsProcedure = object( + { + type: literal("script"), + args: array(unknown), + }, + ["args"], + { + args: [], + }, +) + +const matchProcedure = some(matchDockerProcedure, matchJsProcedure) +export type Procedure = typeof matchProcedure._TYPE + +const matchAction = object( + { + name: string, + description: string, + warning: string, + implementation: matchProcedure, + "allowed-statuses": array(literals("running", "stopped")), + "input-spec": unknown, + }, + ["warning", "input-spec", "input-spec"], +) +export const matchManifest = object( + { + id: string, + title: string, + version: string, + main: matchDockerProcedure, + assets: object( + { + assets: string, + scripts: string, + }, + ["assets", "scripts"], + ), + "health-checks": dictionary([ + string, + every( + matchProcedure, + object( + { + name: string, + ["success-message"]: string, + }, + ["success-message"], + ), + ), + ]), + config: object({ + get: matchProcedure, + set: matchProcedure, + }), + properties: matchProcedure, + volumes: dictionary([string, matchVolume]), + interfaces: dictionary([ + string, + object( + { + name: string, + description: string, + "tor-config": object({ + "port-mapping": dictionary([string, string]), + }), + "lan-config": dictionary([ + string, + object({ + ssl: boolean, + internal: number, + }), + ]), + ui: boolean, + protocols: array(string), + }, + ["lan-config", "tor-config"], + ), + ]), + backup: object({ + create: matchProcedure, + restore: matchProcedure, + }), + migrations: object({ + to: dictionary([string, matchProcedure]), + from: dictionary([string, matchProcedure]), + }), + dependencies: dictionary([ + string, + object( + { + version: string, + requirement: some( + object({ + type: literal("opt-in"), + how: string, + }), + object({ + type: literal("opt-out"), + how: string, + }), + object({ + type: literal("required"), + }), + ), + description: string, + config: object({ + check: matchProcedure, + "auto-configure": matchProcedure, + }), + }, + ["description", "config"], + ), + ]), + + actions: dictionary([string, matchAction]), + }, + ["config", "actions", "properties", "migrations", "dependencies"], +) +export type Manifest = typeof matchManifest._TYPE diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts new file mode 100644 index 000000000..7aa579ecf --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchVolume.ts @@ -0,0 +1,35 @@ +import { object, literal, string, boolean, some } from "ts-matches" + +const matchDataVolume = object( + { + type: literal("data"), + readonly: boolean, + }, + ["readonly"], +) +const matchAssetVolume = object({ + type: literal("assets"), +}) +const matchPointerVolume = object({ + type: literal("pointer"), + "package-id": string, + "volume-id": string, + path: string, + readonly: boolean, +}) +const matchCertificateVolume = object({ + type: literal("certificate"), + "interface-id": string, +}) +const matchBackupVolume = object({ + type: literal("backup"), + readonly: boolean, +}) +export const matchVolume = some( + matchDataVolume, + matchAssetVolume, + matchPointerVolume, + matchCertificateVolume, + matchBackupVolume, +) +export type Volume = typeof matchVolume._TYPE diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts new file mode 100644 index 000000000..73d130c9a --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts @@ -0,0 +1,477 @@ +// deno-lint-ignore no-namespace +export type ExpectedExports = { + version: 2 + /** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */ + setConfig: (effects: Effects, input: Config) => Promise> + /** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */ + getConfig: (effects: Effects) => Promise> + /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ + dependencies: Dependencies + /** For backing up service data though the embassyOS UI */ + createBackup: (effects: Effects) => Promise> + /** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */ + restoreBackup: (effects: Effects) => Promise> + /** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */ + properties: (effects: Effects) => Promise> + health: { + /** Should be the health check id */ + [id: string]: ( + effects: Effects, + dateMs: number, + ) => Promise> + } + migration: ( + effects: Effects, + version: string, + ...args: unknown[] + ) => Promise> + action: { + [id: string]: ( + effects: Effects, + config?: Config, + ) => Promise> + } + + /** + * This is the entrypoint for the main container. Used to start up something like the service that the + * package represents, like running a bitcoind in a bitcoind-wrapper. + */ + main: (effects: Effects) => Promise> +} + +/** Used to reach out from the pure js runtime */ +export type Effects = { + /** Usable when not sandboxed */ + writeFile(input: { + path: string + volumeId: string + toWrite: string + }): Promise + readFile(input: { volumeId: string; path: string }): Promise + metadata(input: { volumeId: string; path: string }): Promise + /** Create a directory. Usable when not sandboxed */ + createDir(input: { volumeId: string; path: string }): Promise + + readDir(input: { volumeId: string; path: string }): Promise + /** Remove a directory. Usable when not sandboxed */ + removeDir(input: { volumeId: string; path: string }): Promise + removeFile(input: { volumeId: string; path: string }): Promise + + /** Write a json file into an object. Usable when not sandboxed */ + writeJsonFile(input: { + volumeId: string + path: string + toWrite: Record + }): Promise + + /** Read a json file into an object */ + readJsonFile(input: { + volumeId: string + path: string + }): Promise> + + runCommand(input: { + command: string + args?: string[] + timeoutMillis?: number + }): Promise> + runDaemon(input: { command: string; args?: string[] }): { + wait(): Promise> + term(): Promise + } + + chown(input: { volumeId: string; path: string; uid: string }): Promise + chmod(input: { volumeId: string; path: string; mode: string }): Promise + + sleep(timeMs: number): Promise + + /** Log at the trace level */ + trace(whatToPrint: string): void + /** Log at the warn level */ + warn(whatToPrint: string): void + /** Log at the error level */ + error(whatToPrint: string): void + /** Log at the debug level */ + debug(whatToPrint: string): void + /** Log at the info level */ + info(whatToPrint: string): void + + /** Sandbox mode lets us read but not write */ + is_sandboxed(): boolean + + // Does a volume and path exist? + exists(input: { volumeId: string; path: string }): Promise + + fetch( + url: string, + options?: { + method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH" + headers?: Record + body?: string + }, + ): Promise<{ + method: string + ok: boolean + status: number + headers: Record + body?: string | null + /// Returns the body as a string + text(): Promise + /// Returns the body as a json + json(): Promise + }> + diskUsage(options?: { + volumeId: string + path: string + }): Promise<{ used: number; total: number }> + + runRsync(options: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + // rsync options: https://linux.die.net/man/1/rsync + options: BackupOptions + }): { + id: () => Promise + wait: () => Promise + progress: () => Promise + } +} + +// rsync options: https://linux.die.net/man/1/rsync +export type BackupOptions = { + delete: boolean + force: boolean + ignoreExisting: boolean + exclude: string[] +} +export type Metadata = { + fileType: string + isDir: boolean + isFile: boolean + isSymlink: boolean + len: number + modified?: Date + accessed?: Date + created?: Date + readonly: boolean + uid: number + gid: number + mode: number +} + +export type MigrationRes = { + configured: boolean +} + +export type ActionResult = { + version: "0" + message: string + value?: string + copyable: boolean + qr: boolean +} + +export type ConfigRes = { + /** This should be the previous config, that way during set config we start with the previous */ + config?: Config + /** Shape that is describing the form in the ui */ + spec: ConfigSpec +} +export type Config = { + [propertyName: string]: unknown +} + +export type ConfigSpec = { + /** Given a config value, define what it should render with the following spec */ + [configValue: string]: ValueSpecAny +} +export type WithDefault = T & { + default: Default +} +export type WithNullableDefault = T & { + default?: Default +} + +export type WithDescription = T & { + description?: string + name: string + warning?: string +} + +export type WithOptionalDescription = T & { + /** @deprecated - optional only for backwards compatibility */ + description?: string + /** @deprecated - optional only for backwards compatibility */ + name?: string + warning?: string +} + +export type ListSpec = { + spec: T + range: string +} + +export type Tag = V & { + type: T +} + +export type Subtype = V & { + subtype: T +} + +export type Target = V & { + target: T +} + +export type UniqueBy = + | { + any: UniqueBy[] + } + | string + | null + +export type WithNullable = T & { + nullable: boolean +} +export type DefaultString = + | string + | { + /** The chars available for the random generation */ + charset?: string + /** Length that we generate to */ + len: number + } + +export type ValueSpecString = // deno-lint-ignore ban-types + ( + | {} + | { + pattern: string + "pattern-description": string + } + ) & { + copyable?: boolean + masked?: boolean + placeholder?: string + } +export type ValueSpecNumber = { + /** Something like [3,6] or [0, *) */ + range?: string + integral?: boolean + /** Used a description of the units */ + units?: string + placeholder?: number +} +export type ValueSpecBoolean = Record +export type ValueSpecAny = + | Tag<"boolean", WithDescription>> + | Tag< + "string", + WithDescription< + WithNullableDefault, DefaultString> + > + > + | Tag< + "number", + WithDescription< + WithNullableDefault, number> + > + > + | Tag< + "enum", + WithDescription< + WithDefault< + { + values: readonly string[] | string[] + "value-names": { + [key: string]: string + } + }, + string + > + > + > + | Tag<"list", ValueSpecList> + | Tag<"object", WithDescription>> + | Tag<"union", WithOptionalDescription>> + | Tag< + "pointer", + WithDescription< + | Subtype< + "package", + | Target< + "tor-key", + { + "package-id": string + interface: string + } + > + | Target< + "tor-address", + { + "package-id": string + interface: string + } + > + | Target< + "lan-address", + { + "package-id": string + interface: string + } + > + | Target< + "config", + { + "package-id": string + selector: string + multi: boolean + } + > + > + | Subtype<"system", Record> + > + > +export type ValueSpecUnion = { + /** What tag for the specification, for tag unions */ + tag: { + id: string + name: string + description?: string + "variant-names": { + [key: string]: string + } + } + /** The possible enum values */ + variants: { + [key: string]: ConfigSpec + } + "display-as"?: string + "unique-by"?: UniqueBy +} +export type ValueSpecObject = { + spec: ConfigSpec + "display-as"?: string + "unique-by"?: UniqueBy +} +export type ValueSpecList = + | Subtype< + "boolean", + WithDescription, boolean[]>> + > + | Subtype< + "string", + WithDescription, string[]>> + > + | Subtype< + "number", + WithDescription, number[]>> + > + | Subtype< + "enum", + WithDescription, string[]>> + > + | Subtype< + "object", + WithDescription< + WithNullableDefault< + ListSpec, + Record[] + > + > + > + | Subtype< + "union", + WithDescription, string[]>> + > +export type ValueSpecEnum = { + values: string[] + "value-names": { [key: string]: string } +} + +export type SetResult = { + /** These are the unix process signals */ + signal: + | "SIGTERM" + | "SIGHUP" + | "SIGINT" + | "SIGQUIT" + | "SIGILL" + | "SIGTRAP" + | "SIGABRT" + | "SIGBUS" + | "SIGFPE" + | "SIGKILL" + | "SIGUSR1" + | "SIGSEGV" + | "SIGUSR2" + | "SIGPIPE" + | "SIGALRM" + | "SIGSTKFLT" + | "SIGCHLD" + | "SIGCONT" + | "SIGSTOP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGURG" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGVTALRM" + | "SIGPROF" + | "SIGWINCH" + | "SIGIO" + | "SIGPWR" + | "SIGSYS" + | "SIGEMT" + | "SIGINFO" + "depends-on": DependsOn +} + +export type DependsOn = { + [packageId: string]: string[] +} + +export type KnownError = + | { error: string } + | { + "error-code": [number, string] | readonly [number, string] + } +export type ResultType = KnownError | { result: T } + +export type PackagePropertiesV2 = { + [name: string]: PackagePropertyObject | PackagePropertyString +} +export type PackagePropertyString = { + type: "string" + description?: string + value: string + /** Let's the ui make this copyable button */ + copyable?: boolean + /** Let the ui create a qr for this field */ + qr?: boolean + /** Hiding the value unless toggled off for field */ + masked?: boolean +} +export type PackagePropertyObject = { + value: PackagePropertiesV2 + type: "object" + description: string +} + +export type Properties = { + version: 2 + data: PackagePropertiesV2 +} + +export type Dependencies = { + /** Id is the id of the package, should be the same as the manifest */ + [id: string]: { + /** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */ + check(effects: Effects, input: Config): Promise> + /** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */ + autoConfigure(effects: Effects, input: Config): Promise> + } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts new file mode 100644 index 000000000..5cbec945a --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -0,0 +1,451 @@ +import * as fs from "fs/promises" +import * as oet from "./oldEmbassyTypes" +import { Volume } from "../../../Models/Volume" +import * as child_process from "child_process" +import { promisify } from "util" +import { daemons, startSdk, T, utils } from "@start9labs/start-sdk" +import "isomorphic-fetch" +import { Manifest } from "./matchManifest" +import { DockerProcedureContainer } from "./DockerProcedureContainer" +import * as cp from "child_process" +import { Effects } from "../../../Models/Effects" +export const execFile = promisify(cp.execFile) +export const polyfillEffects = ( + effects: Effects, + manifest: Manifest, +): oet.Effects => { + const self = { + effects, + manifest, + async writeFile(input: { + path: string + volumeId: string + toWrite: string + }): Promise { + await fs.writeFile( + new Volume(input.volumeId, input.path).path, + input.toWrite, + ) + }, + async readFile(input: { volumeId: string; path: string }): Promise { + return ( + await fs.readFile(new Volume(input.volumeId, input.path).path) + ).toString() + }, + async metadata(input: { + volumeId: string + path: string + }): Promise { + const stats = await fs.stat(new Volume(input.volumeId, input.path).path) + return { + fileType: stats.isFile() ? "file" : "directory", + gid: stats.gid, + uid: stats.uid, + mode: stats.mode, + isDir: stats.isDirectory(), + isFile: stats.isFile(), + isSymlink: stats.isSymbolicLink(), + len: stats.size, + readonly: (stats.mode & 0o200) > 0, + } + }, + async createDir(input: { + volumeId: string + path: string + }): Promise { + const path = new Volume(input.volumeId, input.path).path + await fs.mkdir(path, { recursive: true }) + return path + }, + async readDir(input: { + volumeId: string + path: string + }): Promise { + return fs.readdir(new Volume(input.volumeId, input.path).path) + }, + async removeDir(input: { + volumeId: string + path: string + }): Promise { + const path = new Volume(input.volumeId, input.path).path + await fs.rmdir(new Volume(input.volumeId, input.path).path, { + recursive: true, + }) + return path + }, + removeFile(input: { volumeId: string; path: string }): Promise { + return fs.rm(new Volume(input.volumeId, input.path).path) + }, + async writeJsonFile(input: { + volumeId: string + path: string + toWrite: Record + }): Promise { + await fs.writeFile( + new Volume(input.volumeId, input.path).path, + JSON.stringify(input.toWrite), + ) + }, + async readJsonFile(input: { + volumeId: string + path: string + }): Promise> { + return JSON.parse( + ( + await fs.readFile(new Volume(input.volumeId, input.path).path) + ).toString(), + ) + }, + runCommand({ + command, + args, + timeoutMillis, + }: { + command: string + args?: string[] | undefined + timeoutMillis?: number | undefined + }): Promise> { + const commands: [string, ...string[]] = [command, ...(args || [])] + return startSdk + .runCommand( + effects, + { imageId: manifest.main.image }, + commands, + {}, + commands.join(" "), + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => + !!x.stderr ? { error: x.stderr } : { result: x.stdout }, + ) + }, + runDaemon(input: { command: string; args?: string[] | undefined }): { + wait(): Promise> + term(): Promise + } { + const promiseSubcontainer = DockerProcedureContainer.createSubContainer( + effects, + manifest.id, + manifest.main, + manifest.volumes, + [input.command, ...(input.args || [])].join(" "), + ) + const daemon = promiseSubcontainer.then((subcontainer) => + daemons.runCommand()( + effects, + subcontainer, + [input.command, ...(input.args || [])], + {}, + ), + ) + return { + wait: () => + daemon.then((daemon) => + daemon.wait().then(() => { + return { result: "" } + }), + ), + term: () => daemon.then((daemon) => daemon.term()), + } + }, + async chown(input: { + volumeId: string + path: string + uid: string + }): Promise { + const commands: [string, ...string[]] = [ + "chown", + "--recursive", + input.uid, + `/drive/${input.path}`, + ] + await startSdk + .runCommand( + effects, + { imageId: manifest.main.image }, + commands, + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, + }, + ], + }, + commands.join(" "), + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + }) + return null + }, + async chmod(input: { + volumeId: string + path: string + mode: string + }): Promise { + const commands: [string, ...string[]] = [ + "chmod", + "--recursive", + input.mode, + `/drive/${input.path}`, + ] + await startSdk + .runCommand( + effects, + { imageId: manifest.main.image }, + commands, + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, + }, + ], + }, + commands.join(" "), + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + }) + return null + }, + sleep(timeMs: number): Promise { + return new Promise((resolve) => setTimeout(resolve, timeMs)) + }, + trace(whatToPrint: string): void { + console.trace(utils.asError(whatToPrint)) + }, + warn(whatToPrint: string): void { + console.warn(utils.asError(whatToPrint)) + }, + error(whatToPrint: string): void { + console.error(utils.asError(whatToPrint)) + }, + debug(whatToPrint: string): void { + console.debug(utils.asError(whatToPrint)) + }, + info(whatToPrint: string): void { + console.log(false) + }, + is_sandboxed(): boolean { + return false + }, + exists(input: { volumeId: string; path: string }): Promise { + return self + .metadata(input) + .then(() => true) + .catch(() => false) + }, + async fetch( + url: string, + options?: + | { + method?: + | "GET" + | "POST" + | "PUT" + | "DELETE" + | "HEAD" + | "PATCH" + | undefined + headers?: Record | undefined + body?: string | undefined + } + | undefined, + ): Promise<{ + method: string + ok: boolean + status: number + headers: Record + body?: string | null | undefined + text(): Promise + json(): Promise + }> { + const fetched = await fetch(url, options) + return { + method: fetched.type, + ok: fetched.ok, + status: fetched.status, + headers: Object.fromEntries(fetched.headers.entries()), + body: await fetched.text(), + text: () => fetched.text(), + json: () => fetched.json(), + } + }, + + runRsync(rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: oet.BackupOptions + }): { + id: () => Promise + wait: () => Promise + progress: () => Promise + } { + let secondRun: ReturnType | undefined + let firstRun = self._runRsync(rsyncOptions) + let waitValue = firstRun.wait().then((x) => { + secondRun = self._runRsync(rsyncOptions) + return secondRun.wait() + }) + const id = async () => { + return secondRun?.id?.() ?? firstRun.id() + } + const wait = () => waitValue + const progress = async () => { + const secondProgress = secondRun?.progress?.() + if (secondProgress) { + return (await secondProgress) / 2.0 + 0.5 + } + return (await firstRun.progress()) / 2.0 + } + return { id, wait, progress } + }, + _runRsync(rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: oet.BackupOptions + }): { + id: () => Promise + wait: () => Promise + progress: () => Promise + } { + const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + if (options.force) { + args.push("--force") + } + if (options.ignoreExisting) { + args.push("--ignore-existing") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(new Volume(srcVolume, srcPath).path) + args.push(new Volume(dstVolume, dstPath).path) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(`polyfill.runAsync`, utils.asError(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code: any) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } + }, + async diskUsage( + options?: { volumeId: string; path: string } | undefined, + ): Promise<{ used: number; total: number }> { + const output = await execFile("df", ["--block-size=1", "-P", "/"]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return parseDfOutput(x.stdout) + }) + if (!!options) { + const used = await execFile("du", [ + "-s", + "--block-size=1", + "-P", + new Volume(options.volumeId, options.path).path, + ]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return Number.parseInt(x.stdout.split(/\s+/)[0]) + }) + return { + ...output, + used, + } + } + return output + }, + } + return self +} + +function parseDfOutput(output: string): { used: number; total: number } { + const lines = output + .split("\n") + .filter((x) => x.length) + .map((x) => x.split(/\s+/)) + const index = lines.splice(0, 1)[0].map((x) => x.toLowerCase()) + const usedIndex = index.indexOf("used") + const availableIndex = index.indexOf("available") + const used = lines.map((x) => Number.parseInt(x[usedIndex]))[0] || 0 + const total = lines.map((x) => Number.parseInt(x[availableIndex]))[0] || 0 + return { used, total } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts new file mode 100644 index 000000000..93b43910b --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts @@ -0,0 +1,38 @@ +import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec" +import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" +import searNXG from "./__fixtures__/searNXG" +import bitcoind from "./__fixtures__/bitcoind" +import nostr from "./__fixtures__/nostr" +import nostrConfig2 from "./__fixtures__/nostrConfig2" + +describe("transformConfigSpec", () => { + test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { + matchOldConfigSpec.unsafeCast( + fixtureEmbasyPagesConfig.homepage.variants["web-page"], + ) + }) + test("matchOldConfigSpec(embassyPages)", () => { + matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) + }) + test("transformConfigSpec(embassyPages)", () => { + const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + + test("transformConfigSpec(searNXG)", () => { + const spec = matchOldConfigSpec.unsafeCast(searNXG) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + test("transformConfigSpec(bitcoind)", () => { + const spec = matchOldConfigSpec.unsafeCast(bitcoind) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + test("transformConfigSpec(nostr)", () => { + const spec = matchOldConfigSpec.unsafeCast(nostr) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + test("transformConfigSpec(nostr2)", () => { + const spec = matchOldConfigSpec.unsafeCast(nostrConfig2) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) +}) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts new file mode 100644 index 000000000..1eb2ea508 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -0,0 +1,630 @@ +import { IST } from "@start9labs/start-sdk" +import { + dictionary, + object, + anyOf, + string, + literals, + array, + number, + boolean, + Parser, + deferred, + every, + nill, + literal, +} from "ts-matches" + +export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec { + return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => { + let newVal: IST.ValueSpec + + if (oldVal.type === "boolean") { + newVal = { + type: "toggle", + name: oldVal.name, + default: oldVal.default, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + } + } else if (oldVal.type === "enum") { + newVal = { + type: "select", + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + default: oldVal.default, + values: oldVal.values.reduce( + (obj, curr) => ({ + ...obj, + [curr]: oldVal["value-names"][curr] || curr, + }), + {}, + ), + disabled: false, + immutable: false, + } + } else if (oldVal.type === "list") { + newVal = getListSpec(oldVal) + } else if (oldVal.type === "number") { + const range = Range.from(oldVal.range) + + newVal = { + type: "number", + name: oldVal.name, + default: oldVal.default || null, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + required: !oldVal.nullable, + min: range.min + ? range.minInclusive + ? range.min + : range.min + 1 + : null, + max: range.max + ? range.maxInclusive + ? range.max + : range.max - 1 + : null, + integer: oldVal.integral, + step: null, + units: oldVal.units || null, + placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null, + } + } else if (oldVal.type === "object") { + newVal = { + type: "object", + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)), + } + } else if (oldVal.type === "string") { + newVal = { + type: "text", + name: oldVal.name, + default: oldVal.default || null, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + required: !oldVal.nullable, + patterns: + oldVal.pattern && oldVal["pattern-description"] + ? [ + { + regex: oldVal.pattern, + description: oldVal["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + masked: oldVal.masked || false, + generate: null, + inputmode: "text", + placeholder: oldVal.placeholder || null, + } + } else if (oldVal.type === "union") { + newVal = { + type: "union", + name: oldVal.tag.name, + description: oldVal.tag.description || null, + warning: oldVal.tag.warning || null, + variants: Object.entries(oldVal.variants).reduce( + (obj, [id, spec]) => ({ + ...obj, + [id]: { + name: oldVal.tag["variant-names"][id] || id, + spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)), + }, + }), + {} as Record, + ), + disabled: false, + default: oldVal.default, + immutable: false, + } + } else if (oldVal.type === "pointer") { + return inputSpec + } else { + throw new Error(`unknown spec ${JSON.stringify(oldVal)}`) + } + + return { + ...inputSpec, + [key]: newVal, + } + }, {} as IST.InputSpec) +} + +export function transformOldConfigToNew( + spec: OldConfigSpec, + config: Record, +): Record { + return Object.entries(spec).reduce((obj, [key, val]) => { + let newVal = config[key] + + if (isObject(val)) { + newVal = transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.spec), + config[key], + ) + } + + if (isUnion(val)) { + const selection = config[key][val.tag.id] + delete config[key][val.tag.id] + + newVal = { + selection, + value: transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.variants[selection]), + config[key], + ), + } + } + + if (isList(val) && isObjectList(val)) { + newVal = (config[key] as object[]).map((obj) => + transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.spec.spec), + obj, + ), + ) + } + + if (isPointer(val)) { + return obj + } + + return { + ...obj, + [key]: newVal, + } + }, {}) +} + +export function transformNewConfigToOld( + spec: OldConfigSpec, + config: Record, +): Record { + return Object.entries(spec).reduce((obj, [key, val]) => { + let newVal = config[key] + + if (isObject(val)) { + newVal = transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.spec), + config[key], + ) + } + + if (isUnion(val)) { + newVal = { + [val.tag.id]: config[key].selection, + ...transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]), + config[key].value, + ), + } + } + + if (isList(val) && isObjectList(val)) { + newVal = (config[key] as object[]).map((obj) => + transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.spec.spec), + obj, + ), + ) + } + + return { + ...obj, + [key]: newVal, + } + }, {}) +} + +function getListSpec( + oldVal: OldValueSpecList, +): IST.ValueSpecMultiselect | IST.ValueSpecList { + const range = Range.from(oldVal.range) + + let partial: Omit = { + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + minLength: range.min + ? range.minInclusive + ? range.min + : range.min + 1 + : null, + maxLength: range.max + ? range.maxInclusive + ? range.max + : range.max - 1 + : null, + disabled: false, + } + + if (isEnumList(oldVal)) { + return { + ...partial, + type: "multiselect", + default: oldVal.default as string[], + immutable: false, + values: oldVal.spec.values.reduce( + (obj, curr) => ({ + ...obj, + [curr]: oldVal.spec["value-names"][curr], + }), + {}, + ), + } + } else if (isNumberList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default.map(String) as string[], + spec: { + type: "text", + patterns: oldVal.spec.integral + ? [{ regex: "[0-9]+", description: "Integral number type" }] + : [ + { + regex: "[-+]?[0-9]*\\.?[0-9]+", + description: "Number type", + }, + ], + minLength: null, + maxLength: null, + masked: false, + generate: null, + inputmode: "text", + placeholder: oldVal.spec.placeholder + ? String(oldVal.spec.placeholder) + : null, + }, + } + } else if (isStringList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default as string[], + spec: { + type: "text", + patterns: + oldVal.spec.pattern && oldVal.spec["pattern-description"] + ? [ + { + regex: oldVal.spec.pattern, + description: oldVal.spec["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + masked: oldVal.spec.masked || false, + generate: null, + inputmode: "text", + placeholder: oldVal.spec.placeholder || null, + }, + } + } else if (isObjectList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default as Record[], + spec: { + type: "object", + spec: transformConfigSpec( + matchOldConfigSpec.unsafeCast(oldVal.spec.spec), + ), + uniqueBy: oldVal.spec["unique-by"] || null, + displayAs: oldVal.spec["display-as"] || null, + }, + } + } else { + throw new Error("Invalid list subtype. enum, string, and object permitted.") + } +} + +function isObject(val: OldValueSpec): val is OldValueSpecObject { + return val.type === "object" +} + +function isUnion(val: OldValueSpec): val is OldValueSpecUnion { + return val.type === "union" +} + +function isList(val: OldValueSpec): val is OldValueSpecList { + return val.type === "list" +} + +function isPointer(val: OldValueSpec): val is OldValueSpecPointer { + return val.type === "pointer" +} + +function isEnumList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "enum" } { + return val.subtype === "enum" +} + +function isStringList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "string" } { + return val.subtype === "string" +} +function isNumberList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "number" } { + return val.subtype === "number" +} + +function isObjectList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "object" } { + if (["union"].includes(val.subtype)) { + throw new Error("Invalid list subtype. enum, string, and object permitted.") + } + return val.subtype === "object" +} +export type OldConfigSpec = Record +const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred() +export const matchOldConfigSpec = _matchOldConfigSpec as Parser< + unknown, + OldConfigSpec +> +export const matchOldDefaultString = anyOf( + string, + object({ charset: string, len: number }), +) +type OldDefaultString = typeof matchOldDefaultString._TYPE + +export const matchOldValueSpecString = object( + { + type: literals("string"), + name: string, + masked: boolean, + copyable: boolean, + nullable: boolean, + placeholder: string, + pattern: string, + "pattern-description": string, + default: matchOldDefaultString, + textarea: boolean, + description: string, + warning: string, + }, + [ + "masked", + "copyable", + "nullable", + "placeholder", + "pattern", + "pattern-description", + "default", + "textarea", + "description", + "warning", + ], +) + +export const matchOldValueSpecNumber = object( + { + type: literals("number"), + nullable: boolean, + name: string, + range: string, + integral: boolean, + default: number, + description: string, + warning: string, + units: string, + placeholder: anyOf(number, string), + }, + ["default", "description", "warning", "units", "placeholder"], +) +type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE + +export const matchOldValueSpecBoolean = object( + { + type: literals("boolean"), + default: boolean, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE + +const matchOldValueSpecObject = object( + { + type: literals("object"), + spec: _matchOldConfigSpec, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE + +const matchOldValueSpecEnum = object( + { + values: array(string), + "value-names": dictionary([string, string]), + type: literals("enum"), + default: string, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE + +const matchOldUnionTagSpec = object( + { + id: string, // The name of the field containing one of the union variants + "variant-names": dictionary([string, string]), // The name of each variant + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +const matchOldValueSpecUnion = object({ + type: literals("union"), + tag: matchOldUnionTagSpec, + variants: dictionary([string, _matchOldConfigSpec]), + default: string, +}) +type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE + +const [matchOldUniqueBy, setOldUniqueBy] = deferred() +type OldUniqueBy = + | null + | string + | { any: OldUniqueBy[] } + | { all: OldUniqueBy[] } + +setOldUniqueBy( + anyOf( + nill, + string, + object({ any: array(matchOldUniqueBy) }), + object({ all: array(matchOldUniqueBy) }), + ), +) + +const matchOldListValueSpecObject = object( + { + spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values + "unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list + "display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' + }, + ["display-as", "unique-by"], +) +const matchOldListValueSpecString = object( + { + masked: boolean, + copyable: boolean, + pattern: string, + "pattern-description": string, + placeholder: string, + }, + ["pattern", "pattern-description", "placeholder", "copyable", "masked"], +) + +const matchOldListValueSpecEnum = object({ + values: array(string), + "value-names": dictionary([string, string]), +}) +const matchOldListValueSpecNumber = object( + { + range: string, + integral: boolean, + units: string, + placeholder: anyOf(number, string), + }, + ["units", "placeholder"], +) + +// represents a spec for a list +const matchOldValueSpecList = every( + object( + { + type: literals("list"), + range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules + default: anyOf( + array(string), + array(number), + array(matchOldDefaultString), + array(object), + ), + name: string, + description: string, + warning: string, + }, + ["description", "warning"], + ), + anyOf( + object({ + subtype: literals("string"), + spec: matchOldListValueSpecString, + }), + object({ + subtype: literals("enum"), + spec: matchOldListValueSpecEnum, + }), + object({ + subtype: literals("object"), + spec: matchOldListValueSpecObject, + }), + object({ + subtype: literals("number"), + spec: matchOldListValueSpecNumber, + }), + ), +) +type OldValueSpecList = typeof matchOldValueSpecList._TYPE + +const matchOldValueSpecPointer = every( + object({ + type: literal("pointer"), + }), + anyOf( + object({ + subtype: literal("package"), + target: literals("tor-key", "tor-address", "lan-address"), + "package-id": string, + interface: string, + }), + object({ + subtype: literal("package"), + target: literals("config"), + "package-id": string, + selector: string, + multi: boolean, + }), + ), +) +type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE + +export const matchOldValueSpec = anyOf( + matchOldValueSpecString, + matchOldValueSpecNumber, + matchOldValueSpecBoolean, + matchOldValueSpecObject, + matchOldValueSpecEnum, + matchOldValueSpecList, + matchOldValueSpecUnion, + matchOldValueSpecPointer, +) +type OldValueSpec = typeof matchOldValueSpec._TYPE + +setMatchOldConfigSpec(dictionary([string, matchOldValueSpec])) + +export class Range { + min?: number + max?: number + minInclusive!: boolean + maxInclusive!: boolean + + static from(s: string = "(*,*)"): Range { + const r = new Range() + r.minInclusive = s.startsWith("[") + r.maxInclusive = s.endsWith("]") + const [minStr, maxStr] = s.split(",").map((a) => a.trim()) + r.min = minStr === "(*" ? undefined : Number(minStr.slice(1)) + r.max = maxStr === "*)" ? undefined : Number(maxStr.slice(0, -1)) + return r + } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts new file mode 100644 index 000000000..1d38c83e6 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -0,0 +1,108 @@ +import { System } from "../../Interfaces/System" +import { Effects } from "../../Models/Effects" +import { T, utils } from "@start9labs/start-sdk" +import { Optional } from "ts-matches/lib/parsers/interfaces" + +export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" + +type RunningMain = { + stop: () => Promise +} + +export class SystemForStartOs implements System { + private runningMain: RunningMain | undefined + + static of() { + return new SystemForStartOs(require(STARTOS_JS_LOCATION)) + } + + constructor(readonly abi: T.ABI) { + this + } + async containerInit(effects: Effects): Promise { + return void (await this.abi.containerInit({ effects })) + } + async packageInit( + effects: Effects, + timeoutMs: number | null = null, + ): Promise { + return void (await this.abi.packageInit({ effects })) + } + async packageUninit( + effects: Effects, + nextVersion: Optional = null, + timeoutMs: number | null = null, + ): Promise { + return void (await this.abi.packageUninit({ effects, nextVersion })) + } + async createBackup( + effects: T.Effects, + timeoutMs: number | null, + ): Promise { + return void (await this.abi.createBackup({ + effects, + })) + } + async restoreBackup( + effects: T.Effects, + timeoutMs: number | null, + ): Promise { + return void (await this.abi.restoreBackup({ + effects, + })) + } + getActionInput( + effects: Effects, + id: string, + timeoutMs: number | null, + ): Promise { + const action = this.abi.actions.get(id) + if (!action) throw new Error(`Action ${id} not found`) + return action.getInput({ effects }) + } + runAction( + effects: Effects, + id: string, + input: unknown, + timeoutMs: number | null, + ): Promise { + const action = this.abi.actions.get(id) + if (!action) throw new Error(`Action ${id} not found`) + return action.run({ effects, input }) + } + + async exit(): Promise {} + + async start(effects: Effects): Promise { + if (this.runningMain) return + effects.constRetry = utils.once(() => effects.restart()) + let mainOnTerm: () => Promise | undefined + const started = async (onTerm: () => Promise) => { + await effects.setMainStatus({ status: "running" }) + mainOnTerm = onTerm + return null + } + const daemons = await ( + await this.abi.main({ + effects, + started, + }) + ).build() + this.runningMain = { + stop: async () => { + if (mainOnTerm) await mainOnTerm() + await daemons.term() + }, + } + } + + async stop(): Promise { + if (this.runningMain) { + try { + await this.runningMain.stop() + } finally { + this.runningMain = undefined + } + } + } +} diff --git a/container-runtime/src/Adapters/Systems/index.ts b/container-runtime/src/Adapters/Systems/index.ts new file mode 100644 index 000000000..a44ad533e --- /dev/null +++ b/container-runtime/src/Adapters/Systems/index.ts @@ -0,0 +1,22 @@ +import * as fs from "node:fs/promises" +import { System } from "../../Interfaces/System" +import { EMBASSY_JS_LOCATION, SystemForEmbassy } from "./SystemForEmbassy" +import { STARTOS_JS_LOCATION, SystemForStartOs } from "./SystemForStartOs" +export async function getSystem(): Promise { + if ( + await fs.access(STARTOS_JS_LOCATION).then( + () => true, + () => false, + ) + ) { + return SystemForStartOs.of() + } else if ( + await fs.access(EMBASSY_JS_LOCATION).then( + () => true, + () => false, + ) + ) { + return SystemForEmbassy.of() + } + throw new Error(`${STARTOS_JS_LOCATION} not found`) +} diff --git a/container-runtime/src/Interfaces/AllGetDependencies.ts b/container-runtime/src/Interfaces/AllGetDependencies.ts new file mode 100644 index 000000000..24b68acc5 --- /dev/null +++ b/container-runtime/src/Interfaces/AllGetDependencies.ts @@ -0,0 +1,4 @@ +import { GetDependency } from "./GetDependency" +import { System } from "./System" + +export type AllGetDependencies = GetDependency<"system", Promise> diff --git a/container-runtime/src/Interfaces/GetDependency.ts b/container-runtime/src/Interfaces/GetDependency.ts new file mode 100644 index 000000000..c4bce8733 --- /dev/null +++ b/container-runtime/src/Interfaces/GetDependency.ts @@ -0,0 +1,3 @@ +export type GetDependency = { + [OtherK in K]: () => T +} diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts new file mode 100644 index 000000000..63781cfbd --- /dev/null +++ b/container-runtime/src/Interfaces/System.ts @@ -0,0 +1,50 @@ +import { types as T } from "@start9labs/start-sdk" +import { Effects } from "../Models/Effects" +import { CallbackHolder } from "../Models/CallbackHolder" +import { Optional } from "ts-matches/lib/parsers/interfaces" + +export type Procedure = + | "/packageInit" + | "/packageUninit" + | "/backup/create" + | "/backup/restore" + | `/actions/${string}/getInput` + | `/actions/${string}/run` + +export type ExecuteResult = + | { ok: unknown } + | { err: { code: number; message: string } } +export type System = { + containerInit(effects: T.Effects): Promise + + start(effects: T.Effects): Promise + stop(): Promise + + packageInit(effects: Effects, timeoutMs: number | null): Promise + packageUninit( + effects: Effects, + nextVersion: Optional, + timeoutMs: number | null, + ): Promise + + createBackup(effects: T.Effects, timeoutMs: number | null): Promise + restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise + runAction( + effects: Effects, + actionId: string, + input: unknown, + timeoutMs: number | null, + ): Promise + getActionInput( + effects: Effects, + actionId: string, + timeoutMs: number | null, + ): Promise + + exit(): Promise +} + +export type RunningMain = { + callbacks: CallbackHolder + stop(): Promise +} diff --git a/container-runtime/src/Models/CallbackHolder.ts b/container-runtime/src/Models/CallbackHolder.ts new file mode 100644 index 000000000..ce474268a --- /dev/null +++ b/container-runtime/src/Models/CallbackHolder.ts @@ -0,0 +1,62 @@ +import { T } from "@start9labs/start-sdk" + +const CallbackIdCell = { inc: 1 } + +const callbackRegistry = new FinalizationRegistry( + async (options: { cbs: Map; effects: T.Effects }) => { + await options.effects.clearCallbacks({ + only: Array.from(options.cbs.keys()), + }) + }, +) + +export class CallbackHolder { + constructor(private effects?: T.Effects) {} + + private callbacks = new Map() + private children: WeakRef[] = [] + private newId() { + return CallbackIdCell.inc++ + } + addCallback(callback?: Function) { + if (!callback) { + return + } + const id = this.newId() + console.error("adding callback", id) + this.callbacks.set(id, callback) + if (this.effects) + callbackRegistry.register(this, { + cbs: this.callbacks, + effects: this.effects, + }) + return id + } + child(): CallbackHolder { + const child = new CallbackHolder() + this.children.push(new WeakRef(child)) + return child + } + removeChild(child: CallbackHolder) { + this.children = this.children.filter((c) => { + const ref = c.deref() + return ref && ref !== child + }) + } + private getCallback(index: number): Function | undefined { + let callback = this.callbacks.get(index) + if (callback) this.callbacks.delete(index) + else { + for (let i = 0; i < this.children.length; i++) { + callback = this.children[i].deref()?.getCallback(index) + if (callback) return callback + } + } + return callback + } + callCallback(index: number, args: any[]): Promise { + const callback = this.getCallback(index) + if (!callback) return Promise.resolve() + return Promise.resolve().then(() => callback(...args)) + } +} diff --git a/container-runtime/src/Models/DockerProcedure.ts b/container-runtime/src/Models/DockerProcedure.ts new file mode 100644 index 000000000..20c8145ab --- /dev/null +++ b/container-runtime/src/Models/DockerProcedure.ts @@ -0,0 +1,47 @@ +import { + object, + literal, + string, + boolean, + array, + dictionary, + literals, + number, + Parser, + some, +} from "ts-matches" +import { matchDuration } from "./Duration" + +const VolumeId = string +const Path = string + +export type VolumeId = string +export type Path = string +export const matchDockerProcedure = object( + { + type: literal("docker"), + image: string, + system: boolean, + entrypoint: string, + args: array(string), + mounts: dictionary([VolumeId, Path]), + "io-format": literals( + "json", + "json-pretty", + "yaml", + "cbor", + "toml", + "toml-pretty", + ), + "sigterm-timeout": some(number, matchDuration), + inject: boolean, + }, + ["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"], + { + "sigterm-timeout": 30, + inject: false, + args: [], + }, +) + +export type DockerProcedure = typeof matchDockerProcedure._TYPE diff --git a/container-runtime/src/Models/Duration.ts b/container-runtime/src/Models/Duration.ts new file mode 100644 index 000000000..5f61c362a --- /dev/null +++ b/container-runtime/src/Models/Duration.ts @@ -0,0 +1,30 @@ +import { string } from "ts-matches" + +export type TimeUnit = "d" | "h" | "s" | "ms" | "m" | "µs" | "ns" +export type Duration = `${number}${TimeUnit}` + +const durationRegex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/ + +export const matchDuration = string.refine(isDuration) +export function isDuration(value: string): value is Duration { + return durationRegex.test(value) +} + +export function duration(timeValue: number, timeUnit: TimeUnit = "s") { + return `${timeValue > 0 ? timeValue : 0}${timeUnit}` as Duration +} +const unitsToSeconds: Record = { + ns: 1e-9, + µs: 1e-6, + ms: 0.001, + s: 1, + m: 60, + h: 3600, + d: 86400, +} + +export function fromDuration(duration: Duration | number): number { + if (typeof duration === "number") return duration + const [, num, , unit] = duration.match(durationRegex) || [] + return Number(num) * unitsToSeconds[unit] +} diff --git a/container-runtime/src/Models/Effects.ts b/container-runtime/src/Models/Effects.ts new file mode 100644 index 000000000..bacf8894a --- /dev/null +++ b/container-runtime/src/Models/Effects.ts @@ -0,0 +1,3 @@ +import { types as T } from "@start9labs/start-sdk" + +export type Effects = T.Effects diff --git a/container-runtime/src/Models/JsonPath.ts b/container-runtime/src/Models/JsonPath.ts new file mode 100644 index 000000000..d101836da --- /dev/null +++ b/container-runtime/src/Models/JsonPath.ts @@ -0,0 +1,30 @@ +import { literals, some, string } from "ts-matches" + +type NestedPath = `/${A}/${string}/${B}` +type NestedPaths = NestedPath<"actions", "run" | "getInput"> +// prettier-ignore +type UnNestPaths = + A extends `${infer A}/${infer B}` ? [...UnNestPaths, ... UnNestPaths] : + [A] + +export function unNestPath(a: A): UnNestPaths { + return a.split("/") as UnNestPaths +} +function isNestedPath(path: string): path is NestedPaths { + const paths = path.split("/") + if (paths.length !== 4) return false + if (paths[1] === "actions" && (paths[3] === "run" || paths[3] === "getInput")) + return true + return false +} +export const jsonPath = some( + literals( + "/packageInit", + "/packageUninit", + "/backup/create", + "/backup/restore", + ), + string.refine(isNestedPath, "isNestedPath"), +) + +export type JsonPath = typeof jsonPath._TYPE diff --git a/container-runtime/src/Models/Volume.ts b/container-runtime/src/Models/Volume.ts new file mode 100644 index 000000000..061bb1fd0 --- /dev/null +++ b/container-runtime/src/Models/Volume.ts @@ -0,0 +1,22 @@ +import * as fs from "node:fs/promises" + +export const BACKUP = "backup" +export class Volume { + readonly path: string + constructor( + readonly volumeId: string, + _path = "", + ) { + if (volumeId.toLowerCase() === BACKUP) { + this.path = `/media/startos/backup${!_path ? "" : `/${_path}`}` + } else { + this.path = `/media/startos/volumes/${volumeId}${!_path ? "" : `/${_path}`}` + } + } + async exists() { + return fs.stat(this.path).then( + () => true, + () => false, + ) + } +} diff --git a/container-runtime/src/index.ts b/container-runtime/src/index.ts new file mode 100644 index 000000000..38c0aec1e --- /dev/null +++ b/container-runtime/src/index.ts @@ -0,0 +1,42 @@ +import { RpcListener } from "./Adapters/RpcListener" +import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" +import { AllGetDependencies } from "./Interfaces/AllGetDependencies" +import { getSystem } from "./Adapters/Systems" + +const getDependencies: AllGetDependencies = { + system: getSystem, +} + +new RpcListener(getDependencies) + +/** + +So, this is going to be sent into a running container along with any of the other node modules that are going to be needed and used. + +Once the container is started, we will go into a loading/ await state. +This is the init system, and it will always be running, and it will be waiting for a command to be sent to it. + +Each command will be a stopable promise. And an example is going to be something like an action/ main/ or just a query into the types. + +A command will be sent an object which are the effects, and the effects will be things like the file system, the network, the process, and the os. + + + */ +// So OS Adapter +// ============== + +/** +* Why: So when the we call from the os we enter or leave here? + + */ + +/** +Command: This is a command that the + +There are + */ + +/** +TODO: +Should I separate those adapter in/out? + */ diff --git a/container-runtime/tsconfig.json b/container-runtime/tsconfig.json new file mode 100644 index 000000000..6981133d6 --- /dev/null +++ b/container-runtime/tsconfig.json @@ -0,0 +1,26 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["dist"], + "inputs": ["./src/index.ts"], + "compilerOptions": { + "module": "Node16", + "strict": true, + "outDir": "dist", + "preserveConstEnums": true, + "sourceMap": true, + "target": "ES2022", + "pretty": true, + "declaration": true, + "noImplicitAny": true, + "esModuleInterop": true, + "types": ["node", "jest"], + "moduleResolution": "Node16", + "skipLibCheck": true, + "resolveJsonModule": true + }, + "ts-node": { + "compilerOptions": { + "module": "commonjs" + } + } +} diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh new file mode 100755 index 000000000..a64b4371e --- /dev/null +++ b/container-runtime/update-image.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e + +if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi +if mountpoint tmp/lower; then sudo umount tmp/lower; fi +sudo rm -rf tmp +mkdir -p tmp/lower tmp/upper tmp/work tmp/combined +if which squashfuse > /dev/null; then + sudo squashfuse debian.${ARCH}.squashfs tmp/lower +else + sudo mount debian.${ARCH}.squashfs tmp/lower +fi +sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined + +QEMU= +if [ "$ARCH" != "$(uname -m)" ]; then + QEMU=/usr/bin/qemu-${ARCH}-static + if ! which qemu-$ARCH-static > /dev/null; then + >&2 echo qemu-user-static is required for cross-platform builds + sudo umount tmp/combined + sudo umount tmp/lower + sudo rm -rf tmp + exit 1 + fi + sudo cp $(which qemu-$ARCH-static) tmp/combined${QEMU} +fi + +sudo mkdir -p tmp/combined/usr/lib/startos/ +sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/ +sudo chown -R 0:0 tmp/combined/usr/lib/startos/ +sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runtime.service +sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service +sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli +sudo chown 0:0 tmp/combined/usr/bin/start-cli +echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id +cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash +sudo truncate -s 0 tmp/combined/etc/machine-id + +if [ -n "$QEMU" ]; then + sudo rm tmp/combined${QEMU} +fi + +rm -f rootfs.${ARCH}.squashfs +mkdir -p ../build/lib/container-runtime +sudo mksquashfs tmp/combined rootfs.${ARCH}.squashfs +sudo umount tmp/combined +sudo umount tmp/lower +sudo rm -rf tmp \ No newline at end of file diff --git a/core/.gitignore b/core/.gitignore index ad8368a86..9673044e6 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -8,3 +8,4 @@ secrets.db .env .editorconfig proptest-regressions/**/* +/startos/bindings/* \ No newline at end of file diff --git a/core/Cargo.lock b/core/Cargo.lock index cc7698271..a1e796a15 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -1,16 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] [[package]] name = "addr2line" @@ -27,38 +23,55 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aes" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher 0.3.0", "cpufeatures", "ctr", "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.15", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "cfg-if 1.0.0", - "getrandom 0.2.11", + "cfg-if", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -66,9 +79,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -90,9 +103,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -110,31 +123,87 @@ dependencies = [ ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ascii-canvas" @@ -146,16 +215,76 @@ dependencies = [ ] [[package]] -name = "ast_node" -version = "0.9.5" +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09c69dffe06d222d072c878c3afe86eee2179806f20503faec97250268b4c24" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ - "pmutil", "proc-macro2", "quote", - "swc_macros_common", - "syn 2.0.39", + "syn 2.0.96", +] + +[[package]] +name = "async-acme" +version = "0.6.0" +source = "git+https://github.com/dr-bonez/async-acme.git#0ddf25152237b5fc1726d977a7931e44513ce309" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures-util", + "generic-async-http-client", + "log", + "pem", + "rcgen", + "ring 0.17.8", + "rustls 0.23.21", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "x509-parser", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] @@ -164,16 +293,28 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ - "concurrent-queue", - "event-listener", + "concurrent-queue 2.5.0", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue 2.5.0", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.4" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ "brotli", "flate2", @@ -183,11 +324,113 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue 2.5.0", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue 2.5.0", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -196,24 +439,30 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -226,29 +475,40 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "avahi-sys" -version = "0.10.0" -source = "git+https://github.com/Start9Labs/avahi-sys?branch=feature/dynamic-linking#12bef9e435cfb0d36cb229b9d08e2114c176ea7a" +name = "aws-lc-rs" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea835662a0af02443aa1396d39be523bbf8f11ee6fad20329607c480bea48c3" +dependencies = [ + "aws-lc-sys", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" dependencies = [ "bindgen", - "libc", + "cc", + "cmake", + "dunce", + "fs_extra", + "paste", ] [[package]] @@ -258,13 +518,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 0.1.2", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "base64 0.22.1", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", "itoa", "matchit", "memchr", @@ -273,10 +563,17 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", - "tower", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper 1.0.2", + "tokio", + "tokio-tungstenite 0.24.0", + "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -288,29 +585,77 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "backhand" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2fc1bc7bb7fd449e02000cc1592cc63dcdcd61710f8b9efe32bab2d1784603" +dependencies = [ + "deku", + "flate2", + "rustc-hash", + "thiserror 1.0.69", + "tracing", + "xz2", + "zstd", + "zstd-safe", ] [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] +[[package]] +name = "barrage" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5951c75bdabb58753d140dd5802f12ff3a483cb2e16fb5276e111b94b19e87" +dependencies = [ + "concurrent-queue 1.2.4", + "event-listener 2.5.3", + "spin 0.9.8", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -323,17 +668,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -343,24 +694,15 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-cookies" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" dependencies = [ "lalrpop", "lalrpop-util", "regex", ] -[[package]] -name = "better_scoped_tls" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794edcc9b3fb07bb4aecaa11f093fd45663b4feadb782d68303a2268bc2701de" -dependencies = [ - "scoped-tls", -] - [[package]] name = "bincode" version = "1.3.3" @@ -372,26 +714,25 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.55.1" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b13ce559e6433d360c26305643803cb52cfbabbc2b9c47ce04a58493dfb443" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "cexpr", - "cfg-if 0.1.10", "clang-sys", - "clap 2.34.0", - "env_logger 0.7.1", + "itertools 0.12.1", "lazy_static", "lazycell", "log", - "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "which 3.1.1", + "syn 2.0.96", + "which", ] [[package]] @@ -400,15 +741,30 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", ] [[package]] -name = "bit-vec" -version = "0.6.3" +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -417,18 +773,30 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] [[package]] name = "bitmaps" -version = "3.2.0" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "bitvec" +version = "0.19.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty 1.1.0", + "radium 0.5.3", + "tap", + "wyz 0.2.0", +] [[package]] name = "bitvec" @@ -436,10 +804,10 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty", - "radium", + "funty 2.0.0", + "radium 0.7.0", "tap", - "wyz", + "wyz 0.5.1", ] [[package]] @@ -449,8 +817,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.6", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", "constant_time_eq", + "memmap2", + "rayon-core", ] [[package]] @@ -459,7 +842,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", "generic-array", ] @@ -473,16 +855,23 @@ dependencies = [ ] [[package]] -name = "block-padding" -version = "0.2.1" +name = "blocking" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] [[package]] name = "brotli" -version = "3.4.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -491,9 +880,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -501,9 +890,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -511,35 +906,64 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.5.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] -name = "cc" -version = "1.0.84" +name = "bzip2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ + "bzip2-sys", "libc", ] [[package]] -name = "cexpr" -version = "0.4.0" +name = "bzip2-sys" +version = "0.1.11+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" dependencies = [ - "nom 5.1.3", + "cc", + "libc", + "pkg-config", ] [[package]] -name = "cfg-if" -version = "0.1.10" +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] [[package]] name = "cfg-if" @@ -547,11 +971,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -559,7 +989,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -568,14 +998,14 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.5", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -584,18 +1014,18 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.4.1", ] [[package]] @@ -619,9 +1049,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -630,48 +1060,58 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", + "clap_builder", + "clap_derive", ] [[package]] -name = "clap" -version = "3.2.25" +name = "clap_builder" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ - "atty", - "bitflags 1.3.2", + "anstream", + "anstyle", "clap_lex", - "indexmap 1.9.3", - "strsim 0.10.0", - "termcolor", - "textwrap 0.16.0", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmake" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ - "os_str_bytes", + "cc", ] [[package]] name = "color-eyre" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "color-spantrace", @@ -684,9 +1124,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -694,33 +1134,48 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ - "encode_unicode 0.3.6", - "lazy_static", + "encode_unicode", "libc", - "unicode-width", - "windows-sys 0.45.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] name = "console-api" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" dependencies = [ "futures-core", "prost", @@ -731,16 +1186,17 @@ dependencies = [ [[package]] name = "console-subscriber" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" dependencies = [ "console-api", "crossbeam-channel", "crossbeam-utils", "futures-task", "hdrhistogram", - "humantime 2.1.0", + "humantime", + "prost", "prost-types", "serde", "serde_json", @@ -755,24 +1211,24 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -781,31 +1237,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - -[[package]] -name = "container-init" -version = "0.1.0" -dependencies = [ - "async-stream", - "color-eyre", - "futures", - "helpers", - "imbl", - "nix 0.27.1", - "procfs", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tracing", - "tracing-error", - "tracing-futures", - "tracing-subscriber", - "yajrc 0.1.0 (git+https://github.com/dr-bonez/yajrc.git?branch=develop)", -] +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "convert_case" @@ -824,61 +1258,24 @@ dependencies = [ [[package]] name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "percent-encoding", "time", "version_check", ] -[[package]] -name = "cookie" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" -dependencies = [ - "cookie 0.16.2", - "idna 0.2.3", - "log", - "publicsuffix", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - [[package]] name = "cookie_store" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ - "cookie 0.17.0", - "idna 0.3.0", + "cookie", + "document-features", + "idna 1.0.3", "log", "publicsuffix", "serde", @@ -890,9 +1287,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -900,24 +1297,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -930,40 +1327,80 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if 1.0.0", "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "cfg-if 1.0.0", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.8.0", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "cfg-if 1.0.0", + "winapi", ] [[package]] @@ -974,9 +1411,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -994,21 +1431,11 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -1034,12 +1461,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "current_platform" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc" - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1055,17 +1476,16 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "platforms", - "rustc_version 0.4.0", + "rustc_version", "subtle", "zeroize", ] @@ -1078,14 +1498,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] name = "darling" -version = "0.20.3" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1093,230 +1513,137 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.39", + "strsim 0.11.1", + "syn 2.0.96", ] [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.39", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if 1.0.0", - "hashbrown 0.14.2", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.96", ] [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] -name = "data-url" -version = "0.3.0" +name = "deflate64" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" [[package]] -name = "debugid" -version = "0.8.0" +name = "deku" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +checksum = "709ade444d53896e60f6265660eb50480dd08b77bfc822e5dcc233b88b0b2fba" dependencies = [ - "serde", - "uuid", + "bitvec 1.0.1", + "deku_derive", + "no_std_io", + "rustversion", ] [[package]] -name = "deno-proc-macro-rules" -version = "0.3.2" +name = "deku_derive" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" +checksum = "d7534973f93f9de83203e41c8ddd32d230599fa73fa889f3deb1580ccd186913" dependencies = [ - "deno-proc-macro-rules-macros", + "darling", + "proc-macro-crate", "proc-macro2", - "syn 2.0.39", + "quote", + "syn 2.0.96", ] [[package]] -name = "deno-proc-macro-rules-macros" -version = "0.3.2" +name = "der" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3047b312b7451e3190865713a4dd6e1f821aed614ada219766ebc3024a690435" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.39", + "const-oid", + "der_derive", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "deno_ast" -version = "0.29.5" +name = "der-parser" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8adb6aeb787db71d015d8e9f63f6e004eeb09c86babb4ded00878be18619b1" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ - "anyhow", - "base64 0.13.1", - "deno_media_type", - "dprint-swc-ext", - "serde", - "swc_atoms", - "swc_common", - "swc_config", - "swc_config_macro", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_codegen_macros", - "swc_ecma_loader", - "swc_ecma_parser", - "swc_ecma_transforms_base", - "swc_ecma_transforms_classes", - "swc_ecma_transforms_macros", - "swc_ecma_transforms_proposal", - "swc_ecma_transforms_react", - "swc_ecma_transforms_typescript", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_eq_ignore_macros", - "swc_macros_common", - "swc_visit", - "swc_visit_macros", - "text_lines", - "url", + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", ] [[package]] -name = "deno_core" -version = "0.222.0" +name = "der_derive" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13c81b9ea8462680e7b77088a44fc36390bab3dbfa5a205a285e11b64e0919c" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ - "anyhow", - "bytes", - "deno_ops", - "deno_unsync", - "futures", - "indexmap 2.1.0", - "libc", - "log", - "once_cell", - "parking_lot", - "pin-project", - "serde", - "serde_json", - "serde_v8", - "smallvec", - "sourcemap 7.0.1", - "tokio", - "url", - "v8", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] -name = "deno_media_type" -version = "0.1.2" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a798670c20308e5770cc0775de821424ff9e85665b602928509c8c70430b3ee0" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "data-url", + "powerfmt", "serde", - "url", ] [[package]] -name = "deno_ops" -version = "0.98.0" +name = "derive_arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf89da1a3e50ff7c89956495b53d9bcad29e1f1b3f3d2bc54cad7155f55419c4" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ - "deno-proc-macro-rules", - "lazy-regex", - "once_cell", - "pmutil", - "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "regex", - "strum", - "strum_macros", - "syn 2.0.39", - "thiserror", -] - -[[package]] -name = "deno_unsync" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a8f3722afd50e566ecfc783cc8a3a046bc4dd5eb45007431dfb2776aeb8993" -dependencies = [ - "tokio", -] - -[[package]] -name = "der" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" -dependencies = [ - "powerfmt", - "serde", + "syn 2.0.96", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.0", - "syn 1.0.109", + "rustc_version", + "syn 2.0.96", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.9.0" @@ -1344,7 +1671,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "dirs-sys-next", ] @@ -1359,12 +1686,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "divrem" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1372,47 +1719,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "dprint-swc-ext" -version = "0.12.0" +name = "drain" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0a2492465344a58a37ae119de59e81fe5a2885f2711c7b5048ef0dfa14ce42" +checksum = "9d105028bd2b5dfcb33318fd79a445001ead36004dd8dffef1bdd7e493d8bc1e" dependencies = [ - "bumpalo", - "num-bigint", - "rustc-hash", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "text_lines", + "tokio", ] [[package]] -name = "drain" -version = "0.1.1" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1a0abf3fcefad9b4dd0e414207a7408e12b68414a01e6bb19b897d5bd7632d" -dependencies = [ - "tokio", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature 2.0.0", + "signature 2.2.0", "spki", ] @@ -1433,7 +1770,7 @@ checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "serde", - "signature 2.0.0", + "signature 2.2.0", ] [[package]] @@ -1452,33 +1789,34 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.3", "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", + "subtle", "zeroize", ] [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -1496,30 +1834,25 @@ dependencies = [ [[package]] name = "emver" -version = "0.1.7" -source = "git+https://github.com/Start9Labs/emver-rs.git#61cf0bc96711b4d6f3f30df8efef025e0cc02bad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed260c4d7efaec031b9c4f6c4d3cf136e3df2bbfe50925800236f5e847f28704" dependencies = [ "either", "fp-core", - "nom 7.1.3", + "nom 6.1.2", "serde", ] [[package]] name = "ena" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encode_unicode" version = "1.0.0" @@ -1528,49 +1861,50 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] -name = "env_logger" -version = "0.7.1" +name = "enumflags2" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ - "atty", - "humantime 1.3.0", - "log", - "regex", - "termcolor", + "enumflags2_derive", + "serde", ] [[package]] -name = "env_logger" -version = "0.10.1" +name = "enumflags2_derive" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ - "humantime 2.1.0", - "is-terminal", - "log", - "regex", - "termcolor", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -1581,12 +1915,33 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", "libc", - "windows-sys 0.48.0", ] [[package]] @@ -1595,7 +1950,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "home", "windows-sys 0.48.0", ] @@ -1606,11 +1961,50 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue 2.5.0", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "exver" +version = "0.2.0" +source = "git+https://github.com/Start9Labs/exver-rs.git#29f52c1be18a0fe187670beac92822994b0d1949" +dependencies = [ + "either", + "emver", + "fp-core", + "getrandom 0.2.15", + "itertools 0.13.0", + "memchr", + "pest", + "pest_derive", + "serde", + "smallvec", + "yasi", +] + [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -1618,9 +2012,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock-rs" @@ -1643,28 +2037,22 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.3" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "libredox", + "windows-sys 0.59.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -1673,19 +2061,19 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.3", ] [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -1715,9 +2103,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1732,26 +2120,16 @@ dependencies = [ ] [[package]] -name = "from_variant" -version = "0.1.6" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ec5dc38ee19078d84a692b1c41181ff9f94331c76cee66ff0208c770b5e54f" -dependencies = [ - "pmutil", - "proc-macro2", - "swc_macros_common", - "syn 2.0.39", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "fslock" -version = "0.1.8" +name = "funty" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57eafdd0c16f57161105ae1b98a1238f97645f2f588438b2949c99a2af9616bf" -dependencies = [ - "libc", - "winapi", -] +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "funty" @@ -1761,9 +2139,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1776,9 +2154,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1786,15 +2164,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1814,38 +2192,62 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", +] + +[[package]] +name = "futures-rustls" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" +dependencies = [ + "futures-io", + "rustls 0.22.4", + "rustls-pki-types", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1870,39 +2272,68 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-async-http-client" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cec8bb4d3d32542cfcb9517f78366b52c17931e30d7ee1682c13686c19cee7" +dependencies = [ + "futures", + "futures-rustls", + "hyper 1.5.2", + "log", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.25.0", + "webpki-roots 0.26.7", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gpt" @@ -1910,7 +2341,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.8.0", "crc", "log", "uuid", @@ -1929,17 +2360,36 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 1.9.3", + "http 0.2.12", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -1948,9 +2398,19 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "half" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] [[package]] name = "hashbrown" @@ -1964,35 +2424,41 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", ] [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.5", ] [[package]] name = "hdrhistogram" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b38e5c02b7c7be48c8dc5217c4f1634af2ea221caae2e024bffc7a7651c691" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64 0.13.1", + "base64 0.21.7", "byteorder", "flate2", "nom 7.1.3", @@ -2008,6 +2474,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "helpers" version = "0.1.0" @@ -2017,28 +2489,25 @@ dependencies = [ "lazy_async_pool", "models", "pin-project", + "rpc-toolkit", "serde", "serde_json", "tokio", "tokio-stream", "tracing", - "yajrc 0.1.0 (git+https://github.com/dr-bonez/yajrc.git?branch=develop)", ] [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -2047,28 +2516,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hifijson" -version = "0.2.0" +name = "hickory-proto" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 1.0.3", + "ipnet", + "once_cell", + "rand 0.8.5", + "ring 0.16.20", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tokio-rustls 0.24.1", + "tracing", + "url", +] [[package]] -name = "hkdf" -version = "0.12.3" +name = "hickory-resolver" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" dependencies = [ - "hmac 0.12.1", + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "rustls 0.21.12", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tracing", ] [[package]] -name = "hmac" -version = "0.11.0" +name = "hifijson" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9958ab3ce3170c061a27679916bd9b969eceeb5e8b120438e6751d0987655c42" + +[[package]] +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "crypto-mac", - "digest 0.9.0", + "hmac", ] [[package]] @@ -2082,18 +2592,40 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", ] [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -2102,20 +2634,43 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "http", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -2123,15 +2678,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] - [[package]] name = "humantime" version = "2.1.0" @@ -2140,35 +2686,73 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.5.2", + "hyper-util", + "rustls 0.23.21", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.32", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2176,39 +2760,44 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper", + "http-body-util", + "hyper 1.5.2", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] -name = "hyper-ws-listener" -version = "0.3.0" +name = "hyper-util" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbfe4981e45b0a7403a55d4af12f8d30e173e722409658c3857243990e72180" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ - "anyhow", - "base64 0.21.5", - "env_logger 0.10.1", - "futures", - "hyper", - "log", - "sha-1", + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.2", + "pin-project-lite", + "socket2", "tokio", - "tokio-tungstenite", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2227,6 +2816,133 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "id-pool" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d0df4d8a768821ee4aa2e0353f67125c4586f0e13adbf95b8ebbf8d8fdb344" +dependencies = [ + "serde", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2235,40 +2951,45 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" -version = "0.3.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "idna" -version = "0.4.0" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] -name = "if_chain" -version = "1.0.2" +name = "image" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", +] [[package]] name = "imbl" @@ -2286,17 +3007,29 @@ dependencies = [ [[package]] name = "imbl-sized-chunks" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6957ea0b2541c5ca561d3ef4538044af79f8a05a1eb3a3b148936aaceaa1076" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" dependencies = [ "bitmaps", ] [[package]] name = "imbl-value" -version = "0.1.0" -source = "git+https://github.com/Start9Labs/imbl-value.git#929395141c3a882ac366c12ac9402d0ebaa2201b" +version = "0.1.1" +source = "git+https://github.com/Start9Labs/imbl-value.git#1900943e17116def03bf00bff05cf12e54d810bc" +dependencies = [ + "imbl", + "serde", + "serde_json", + "yasi", +] + +[[package]] +name = "imbl-value" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3431be119ebf79f4bd0ce420dfa66aaeffbc4688f79c796237dc1f3b2d6d71" dependencies = [ "imbl", "serde", @@ -2307,18 +3040,18 @@ dependencies = [ [[package]] name = "include_dir" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ "proc-macro2", "quote", @@ -2343,27 +3076,27 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.15.2", "serde", ] [[package]] name = "indicatif" -version = "0.17.7" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", "tokio", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -2376,30 +3109,32 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.12" +name = "integer-encoding" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" dependencies = [ - "cfg-if 1.0.0", + "async-trait", + "tokio", ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "ipconfig" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "hermit-abi 0.3.3", - "libc", + "socket2", + "widestring", "windows-sys 0.48.0", + "winreg", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" dependencies = [ "serde", ] @@ -2415,28 +3150,21 @@ dependencies = [ ] [[package]] -name = "is-macro" -version = "0.3.0" +name = "is-terminal" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "syn 2.0.39", + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.3", - "rustix 0.38.21", - "windows-sys 0.48.0", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "isocountry" @@ -2445,7 +3173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ea1dc4bf0fb4904ba83ffdb98af3d9c325274e92e6e295e4151e86c96363e04" dependencies = [ "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2476,10 +3204,28 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.9" +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jaq-core" @@ -2487,7 +3233,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb52eeac20f256459e909bd4a03bb8c4fab6a1fdbb8ed52d00f644152df48ece" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", "dyn-clone", "hifijson", "indexmap 1.9.3", @@ -2519,51 +3265,40 @@ dependencies = [ "jaq-parse", ] +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "josekit" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5754487a088f527b1407df470db8e654e4064dccbbe1fe850e0773721e9962b7" +checksum = "54b85e2125819afc4fd2ae57416207e792c7e12797858e5db2a6c6f24a166829" dependencies = [ "anyhow", - "base64 0.21.5", + "base64 0.22.1", "flate2", "once_cell", "openssl", "regex", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "time", ] -[[package]] -name = "js-engine" -version = "0.1.0" -dependencies = [ - "async-trait", - "container-init", - "dashmap", - "deno_ast", - "deno_core", - "helpers", - "itertools 0.11.0", - "lazy_static", - "models", - "reqwest", - "serde", - "serde_json", - "sha2 0.10.8", - "tokio", - "tracing", -] - [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2571,7 +3306,7 @@ dependencies = [ name = "json-patch" version = "0.2.7-alpha.0" dependencies = [ - "imbl-value", + "imbl-value 0.1.2", "json-ptr", "serde", "treediff", @@ -2582,9 +3317,9 @@ name = "json-ptr" version = "0.1.0" dependencies = [ "imbl", - "imbl-value", + "imbl-value 0.1.2", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2592,7 +3327,7 @@ name = "jsonpath_lib" version = "0.3.0" source = "git+https://github.com/Start9Labs/jsonpath.git#1cacbd64afa2e1941a21fef06bad14317ba92f30" dependencies = [ - "imbl-value", + "imbl-value 0.1.1", "log", "serde", "serde_json", @@ -2600,65 +3335,42 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "lalrpop" -version = "0.19.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", - "diff", + "bit-set 0.5.3", "ena", - "is-terminal", - "itertools 0.10.5", + "itertools 0.11.0", "lalrpop-util", "petgraph", + "pico-args", "regex", - "regex-syntax 0.6.29", + "regex-syntax 0.8.5", "string_cache", "term", "tiny-keccak", "unicode-xid", + "walkdir", ] [[package]] name = "lalrpop-util" -version = "0.19.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" -dependencies = [ - "regex", -] - -[[package]] -name = "lazy-regex" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" -dependencies = [ - "lazy-regex-proc_macros", - "once_cell", - "regex", -] - -[[package]] -name = "lazy-regex-proc_macros" -version = "3.1.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.39", + "regex-automata 0.4.9", ] [[package]] @@ -2667,17 +3379,23 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06cf485d4867e0714e35c1652e736bcf892d28fceecca01036764575db64ba84" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures", ] +[[package]] +name = "lazy_format" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e479e99b287d578ed5f6cd4c92cdf48db219088adb9c5b14f7c155b71dfba792" + [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin 0.9.8", ] [[package]] @@ -2686,44 +3404,57 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec 0.5.2", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" -version = "0.2.150" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ - "cfg-if 1.0.0", - "winapi", + "cfg-if", + "windows-targets 0.52.6", ] [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.8.0", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.8", ] [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -2731,32 +3462,150 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.1.4" +name = "libyml" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e281a65eeba3d4503a2839252f86374528f9ceafe6fed97c1d3b52e1fb625c1" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" -version = "0.4.20" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "mail-auth" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd9d657de66a3d5ac360c3eab8c9f5cac2565f2b97cc032d5de4c900ef470de" +dependencies = [ + "ahash 0.8.11", + "flate2", + "hickory-resolver", + "lru-cache", + "mail-builder", + "mail-parser", + "parking_lot", + "quick-xml", + "ring 0.17.8", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "zip", +] + +[[package]] +name = "mail-builder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f5871d5270ed80f2ee750b95600c8d69b05f8653ad3be913b2ad2e924fefcb" +dependencies = [ + "gethostname", +] + +[[package]] +name = "mail-parser" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "mail-send" +version = "0.4.9" +source = "git+https://github.com/dr-bonez/mail-send.git?branch=main#57545dadab5808d59145d133de64f81b8ba01979" +dependencies = [ + "base64 0.22.1", + "gethostname", + "mail-auth", + "mail-builder", + "md5", + "rand 0.8.5", + "rustls 0.23.21", + "rustls-pki-types", + "smtp-proto", + "tokio", + "tokio-rustls 0.26.1", + "webpki-roots 0.26.7", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" @@ -2767,12 +3616,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.7.3" @@ -2786,10 +3629,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c487024623ae38584610237dd1be8932bb2b324474b23c37a25f9fbe6bf5e9e" dependencies = [ "bincode", - "bitvec", + "bitvec 1.0.1", "serde", "serde-big-array", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2798,15 +3641,30 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "digest 0.10.7", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] [[package]] name = "memoffset" @@ -2826,6 +3684,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2840,59 +3707,73 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.9" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "models" version = "0.1.0" dependencies = [ - "base64 0.21.5", + "axum 0.7.9", + "base64 0.21.7", "color-eyre", - "ed25519-dalek 2.0.0", - "emver", + "ed25519-dalek 2.1.1", + "exver", "ipnet", "lazy_static", "mbrman", + "num_enum", "openssl", "patch-db", "rand 0.8.5", "regex", "reqwest", "rpc-toolkit", + "rustls 0.23.21", "serde", "serde_json", "sqlx", "ssh-key", - "thiserror", + "thiserror 1.0.69", "tokio", "torut", "tracing", + "ts-rs", "yasi", + "zbus", ] [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2906,15 +3787,15 @@ dependencies = [ [[package]] name = "new_debug_unreachable" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "new_mime_guess" -version = "4.0.1" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d684d1b59e0dc07b37e2203ef576987473288f530082512aff850585c61b1f" +checksum = "02a2dfb3559d53e90b709376af1c379462f7fb3085a0177deb73e6ea0d99eff4" dependencies = [ "mime", "unicase", @@ -2927,7 +3808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "libc", "memoffset 0.6.5", ] @@ -2939,7 +3820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "libc", "memoffset 0.7.1", "pin-utils", @@ -2947,21 +3828,35 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.4.1", - "cfg-if 1.0.0", + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases", "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "no_std_io" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5f306a6f2c01b4fd172f29bb46195b1764061bf926c75e96ff55df3178208" +dependencies = [ + "memchr", ] [[package]] name = "nom" -version = "5.1.3" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ + "bitvec 0.19.6", + "funty 1.1.0", + "lexical-core", "memchr", "version_check", ] @@ -2988,9 +3883,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -3002,15 +3897,12 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", - "rand 0.8.5", - "serde", ] [[package]] @@ -3032,28 +3924,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -3062,11 +3959,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -3074,9 +3970,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -3088,29 +3984,29 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 2.0.0", + "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -3121,46 +4017,55 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" -version = "1.18.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssh-keys" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" +checksum = "abb830a82898b2ac17c9620ddce839ac3b34b9cb8a1a037cbdbfb9841c756c3e" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "byteorder", "md-5", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.4.1", - "cfg-if 1.0.0", + "bitflags 2.8.0", + "cfg-if", "foreign-types", "libc", "once_cell", @@ -3176,7 +4081,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -3187,18 +4092,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.6+3.1.4" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -3208,10 +4113,14 @@ dependencies = [ ] [[package]] -name = "os_str_bytes" -version = "6.6.1" +name = "ordered-stream" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] [[package]] name = "overload" @@ -3250,33 +4159,53 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.12.1" +name = "p521" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.8", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.8", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "patch-db" @@ -3286,15 +4215,15 @@ dependencies = [ "fd-lock-rs", "futures", "imbl", - "imbl-value", + "imbl-value 0.1.2", "json-patch", "json-ptr", "lazy_static", "nix 0.26.4", "patch-db-macro", "serde", - "serde_cbor 0.11.1", - "thiserror", + "serde_cbor", + "thiserror 1.0.69", "tokio", "tracing", "tracing-error", @@ -3313,18 +4242,12 @@ dependencies = [ name = "patch-db-macro-internals" version = "0.1.0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -3332,14 +4255,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac 0.12.1", + "hmac", ] [[package]] -name = "peeking_take_while" -version = "0.1.2" +name = "pem" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] [[package]] name = "pem-rfc7468" @@ -3352,53 +4279,63 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "petgraph" -version = "0.6.4" +name = "pest" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ - "fixedbitset", - "indexmap 2.1.0", + "memchr", + "thiserror 2.0.11", + "ucd-trie", ] [[package]] -name = "phf" -version = "0.10.1" +name = "pest_derive" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", + "pest", + "pest_generator", ] [[package]] -name = "phf_generator" -version = "0.10.0" +name = "pest_generator" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ - "phf_shared", - "rand 0.8.5", + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] -name = "phf_macros" -version = "0.10.0" +name = "pest_meta" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", + "once_cell", + "pest", + "sha2 0.10.8", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.7.0", ] [[package]] @@ -3410,31 +4347,37 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3442,6 +4385,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3465,32 +4419,30 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "platforms" -version = "3.2.0" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] -name = "pmutil" -version = "0.6.1" +name = "polling" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "cfg-if", + "concurrent-queue 2.5.0", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", ] [[package]] name = "portable-atomic" -version = "1.5.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -3500,9 +4452,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "precomputed-hash" @@ -3510,6 +4465,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -3517,86 +4482,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" dependencies = [ "csv", - "encode_unicode 1.0.0", + "encode_unicode", "is-terminal", "lazy_static", "term", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "primeorder" -version = "0.13.3" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.22.22", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 1.3.2", - "byteorder", + "bitflags 2.8.0", "chrono", "flate2", "hex", "lazy_static", - "rustix 0.36.17", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.8.0", + "chrono", + "hex", ] [[package]] name = "proptest" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.4.1", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.8.0", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.2", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -3604,20 +4564,20 @@ dependencies = [ [[package]] name = "proptest-derive" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.96", ] [[package]] name = "prost" -version = "0.12.1" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -3625,22 +4585,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.1" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] name = "prost-types" -version = "0.12.1" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -3652,22 +4612,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] -name = "psm" -version = "0.1.21" +name = "publicsuffix" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" dependencies = [ - "cc", + "idna 1.0.3", + "psl-types", ] [[package]] -name = "publicsuffix" -version = "2.2.3" +name = "qrcode" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" dependencies = [ - "idna 0.3.0", - "psl-types", + "image", ] [[package]] @@ -3676,15 +4636,30 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quote" -version = "1.0.33" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "radium" version = "0.7.0" @@ -3750,7 +4725,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.15", ] [[package]] @@ -3781,59 +4756,72 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.1.57" +name = "rayon-core" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "rcgen" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" dependencies = [ - "bitflags 1.3.2", + "pem", + "ring 0.17.8", + "time", + "yasna", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -3847,13 +4835,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.5", ] [[package]] @@ -3864,28 +4852,31 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "bytes", - "cookie 0.16.2", - "cookie_store 0.16.2", + "cookie", + "cookie_store", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -3894,57 +4885,86 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile 2.2.0", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper 1.0.2", "system-configuration", "tokio", "tokio-native-tls", "tokio-socks", "tokio-util", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] name = "reqwest_cookie_store" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061" +checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9" dependencies = [ "bytes", - "cookie_store 0.20.0", + "cookie_store", "reqwest", "url", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] [[package]] name = "ring" -version = "0.17.5" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "getrandom 0.2.11", + "cfg-if", + "getrandom 0.2.15", "libc", "spin 0.9.8", - "untrusted", - "windows-sys 0.48.0", + "untrusted 0.9.0", + "windows-sys 0.52.0", ] [[package]] @@ -3960,53 +4980,37 @@ dependencies = [ [[package]] name = "rpc-toolkit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5353673ffd8265292281141560d2b851e4da49e83e2f5e255fd473736d45ee10" +version = "0.3.0" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=master#0747acc54b3dafa8689db1365fba5b28a03806b1" dependencies = [ - "clap 3.2.25", + "async-stream", + "async-trait", + "axum 0.7.9", + "clap", "futures", - "hyper", + "http 1.2.0", + "http-body-util", + "imbl-value 0.1.2", + "itertools 0.12.1", + "lazy_format", "lazy_static", "openssl", + "pin-project", "reqwest", - "rpc-toolkit-macro", "serde", - "serde_cbor 0.11.2", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", + "tokio-stream", "url", - "yajrc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rpc-toolkit-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e4b9cb00baf2d61bcd35e98d67dcb760382a3b4540df7e63b38d053c8a7b8b" -dependencies = [ - "proc-macro2", - "rpc-toolkit-macro-internals", - "syn 1.0.109", -] - -[[package]] -name = "rpc-toolkit-macro-internals" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e2ce21b936feaecdab9c9a8e75b9dca64374ccc11951a58045ad6559b75f42" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "yajrc", ] [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest 0.10.7", @@ -4017,7 +5021,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "spki", "subtle", "zeroize", @@ -4035,20 +5039,20 @@ dependencies = [ [[package]] name = "rust-argon2" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e71971821b3ae0e769e4a4328dbcb517607b434db7697e9aba17203ec14e46a" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "blake2b_simd", "constant_time_eq", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4058,59 +5062,75 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.2.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 0.9.0", + "semver", ] [[package]] -name = "rustc_version" -version = "0.4.0" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "semver 1.0.20", + "nom 7.1.3", ] [[package]] name = "rustix" -version = "0.36.17" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", + "bitflags 2.8.0", + "errno 0.3.10", "libc", - "linux-raw-sys 0.1.4", - "windows-sys 0.45.0", + "linux-raw-sys", + "windows-sys 0.59.0", ] [[package]] -name = "rustix" -version = "0.38.21" +name = "rustls" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "bitflags 2.4.1", - "errno", - "libc", - "linux-raw-sys 0.4.11", - "windows-sys 0.48.0", + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", ] [[package]] name = "rustls" -version = "0.21.8" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring", - "rustls-webpki", - "sct", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", ] [[package]] @@ -4119,24 +5139,51 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rusty-fork" @@ -4150,26 +5197,44 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustyline-async" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3f78c2ea57b827be4c11adbfed26e5fe1b49fb6fb7826e2a9eebbc2e8db10" +dependencies = [ + "crossterm", + "futures-util", + "pin-project", + "thingbuf", + "thiserror 2.0.11", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] -name = "schannel" -version = "0.1.22" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "windows-sys 0.48.0", + "winapi-util", ] [[package]] -name = "scoped-tls" -version = "1.0.1" +name = "schannel" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] [[package]] name = "scopeguard" @@ -4183,8 +5248,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -4203,11 +5268,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -4216,9 +5281,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -4226,33 +5291,18 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" -version = "1.0.192" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -4267,60 +5317,74 @@ dependencies = [ ] [[package]] -name = "serde_bytes" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +name = "serde_cbor" +version = "0.11.1" dependencies = [ + "half 1.8.3", "serde", ] [[package]] -name = "serde_cbor" -version = "0.11.1" +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ - "half", - "serde", + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] -name = "serde_cbor" -version = "0.11.2" +name = "serde_json" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ - "half", + "indexmap 2.7.0", + "itoa", + "memchr", + "ryu", "serde", ] [[package]] -name = "serde_derive" -version = "1.0.192" +name = "serde_path_to_error" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "itoa", + "serde", ] [[package]] -name = "serde_json" -version = "1.0.108" +name = "serde_qs" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" dependencies = [ - "indexmap 2.1.0", - "itoa", - "ryu", + "percent-encoding", "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -4337,34 +5401,19 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_v8" -version = "0.131.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cafa16d0a4288d75925351bb54d06d2e830118ad3fad393947bb11f91b18f3" -dependencies = [ - "bytes", - "derive_more", - "num-bigint", - "serde", - "serde_bytes", - "smallvec", - "thiserror", - "v8", -] - [[package]] name = "serde_with" -version = "3.4.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.1.0", + "indexmap 2.7.0", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -4372,38 +5421,31 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.4.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] -name = "serde_yaml" -version = "0.9.27" +name = "serde_yml" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "78ce6afeda22f0b55dde2c34897bce76a629587348480384231205c14b59a01f" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.7.0", "itoa", + "libyml", + "log", + "memchr", "ryu", "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", + "serde_json", + "tempfile", ] [[package]] @@ -4412,7 +5454,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] @@ -4424,7 +5466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -4436,21 +5478,19 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.9.1" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", + "digest 0.10.7", "keccak", - "opaque-debug", ] [[package]] @@ -4462,17 +5502,44 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" -version = "0.1.1" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -4485,14 +5552,20 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simple-logging" version = "2.0.2" @@ -4521,80 +5594,30 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "smartstring" -version = "1.0.1" +name = "smawk" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "snapshot_creator" -version = "0.1.0" -dependencies = [ - "dashmap", - "deno_ast", - "deno_core", -] +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] -name = "socket2" -version = "0.4.10" +name = "smtp-proto" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] +checksum = "51b8ad3dd187f0d4debab02ad65405a9919d6a4f7bce25bd64a258781063a53a" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "sourcemap" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4cbf65ca7dc576cf50e21f8d0712d96d4fcfd797389744b7b222a85cdf5bd90" -dependencies = [ - "data-encoding", - "debugid", - "if_chain", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id", - "url", -] - -[[package]] -name = "sourcemap" -version = "7.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10da010a590ed2fa9ca8467b00ce7e9c5a8017742c0c09c45450efc172208c4b" -dependencies = [ - "data-encoding", - "debugid", - "if_chain", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id", - "url", + "windows-sys 0.52.0", ] [[package]] @@ -4614,9 +5637,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -4624,20 +5647,19 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.2" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "itertools 0.11.0", "nom 7.1.3", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4648,20 +5670,19 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "atoi", "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", - "dotenvy", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -4669,32 +5690,32 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.1.0", + "indexmap 2.7.0", "log", "memchr", "once_cell", "paste", "percent-encoding", - "rustls", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "serde", "serde_json", "sha2 0.10.8", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", "url", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -4705,13 +5726,13 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -4731,13 +5752,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.8.0", "byteorder", "bytes", "chrono", @@ -4752,7 +5773,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "itoa", "log", "md-5", @@ -4767,20 +5788,20 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.8.0", "byteorder", "chrono", "crc", @@ -4792,7 +5813,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -4802,21 +5823,20 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha1", "sha2 0.10.8", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "chrono", @@ -4833,6 +5853,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "urlencoding", ] [[package]] @@ -4858,8 +5879,8 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.39", - "unicode-width", + "syn 2.0.96", + "unicode-width 0.1.14", ] [[package]] @@ -4885,18 +5906,19 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.6.2" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2180b3bc4955efd5661a97658d3cf4c8107e0d132f619195afe9486c13cca313" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" dependencies = [ - "ed25519-dalek 2.0.0", + "ed25519-dalek 2.1.1", "p256", "p384", + "p521", "rand_core 0.6.4", "rsa", "sec1", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "ssh-cipher", "ssh-encoding", "subtle", @@ -4909,82 +5931,82 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" -dependencies = [ - "cc", - "cfg-if 1.0.0", - "libc", - "psm", - "winapi", -] - [[package]] name = "start-os" -version = "0.3.5-rev.1" +version = "0.3.6-alpha.13" dependencies = [ - "aes", + "aes 0.7.5", + "async-acme", "async-compression", "async-stream", "async-trait", - "avahi-sys", - "base32", - "base64 0.21.5", + "axum 0.7.9", + "backhand", + "barrage", + "base32 0.5.1", + "base64 0.22.1", "base64ct", "basic-cookies", + "blake3", "bytes", "chrono", "ciborium", - "clap 3.2.25", + "clap", "color-eyre", "console", "console-subscriber", - "container-init", - "cookie 0.18.0", - "cookie_store 0.20.0", - "current_platform", + "const_format", + "cookie", + "cookie_store", + "der", "digest 0.10.7", "divrem", "ed25519 2.2.3", "ed25519-dalek 1.0.1", - "ed25519-dalek 2.0.0", - "emver", + "ed25519-dalek 2.1.1", + "exver", "fd-lock-rs", + "form_urlencoded", "futures", "gpt", "helpers", "hex", - "hmac 0.12.1", - "http", - "hyper", - "hyper-ws-listener", + "hmac", + "http 1.2.0", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "id-pool", "imbl", - "imbl-value", + "imbl-value 0.1.2", "include_dir", - "indexmap 2.1.0", + "indexmap 2.7.0", "indicatif", + "integer-encoding", "ipnet", "iprange", "isocountry", - "itertools 0.11.0", + "itertools 0.13.0", "jaq-core", "jaq-std", "josekit", - "js-engine", "jsonpath_lib", + "lazy_async_pool", + "lazy_format", "lazy_static", "libc", "log", + "mail-send", "mbrman", + "mio", "models", "new_mime_guess", - "nix 0.27.1", + "nix 0.29.0", "nom 7.1.3", "num", + "num_cpus", "num_enum", + "once_cell", "openssh-keys", "openssl", "p256", @@ -4993,8 +6015,10 @@ dependencies = [ "pin-project", "pkcs8", "prettytable-rs", + "procfs", "proptest", "proptest-derive", + "qrcode", "rand 0.8.5", "regex", "reqwest", @@ -5002,39 +6026,50 @@ dependencies = [ "rpassword", "rpc-toolkit", "rust-argon2", - "scopeguard", - "semver 1.0.20", + "rustls 0.23.21", + "rustls-pki-types", + "rustyline-async", + "semver", "serde", "serde_json", + "serde_urlencoded", "serde_with", - "serde_yaml", + "serde_yml", "sha2 0.10.8", + "shell-words", + "signal-hook", "simple-logging", + "socket2", "sqlx", "sscanf", "ssh-key", - "stderrlog", "tar", - "thiserror", + "textwrap", + "thiserror 1.0.69", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.1", "tokio-socks", "tokio-stream", "tokio-tar", - "tokio-tungstenite", + "tokio-tungstenite 0.23.1", "tokio-util", - "toml 0.8.8", + "toml 0.8.19", "torut", + "tower-service", "tracing", "tracing-error", "tracing-futures", "tracing-journald", "tracing-subscriber", "trust-dns-server", + "ts-rs", + "tty-spawn", "typed-builder", + "unix-named-pipe", "url", "urlencoding", "uuid", + "zbus", "zeroize", ] @@ -5044,19 +6079,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stderrlog" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a26bbf6de627d389164afa9783739b56746c6c72c4ed16539f4ff54170327b" -dependencies = [ - "atty", - "chrono", - "log", - "termcolor", - "thread_local", -] - [[package]] name = "string_cache" version = "0.8.7" @@ -5068,467 +6090,101 @@ dependencies = [ "parking_lot", "phf_shared", "precomputed-hash", - "serde", ] [[package]] -name = "string_cache_codegen" -version = "0.5.2" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] -name = "string_enum" -version = "0.4.1" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa4d4f81d7c05b9161f8de839975d3326328b8ba2831164b465524cc2f55252" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.39", -] - -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.39", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "swc_atoms" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f54563d7dcba626d4acfe14ed12def7ecc28e004debe3ecd2c3ee07cc47e449" -dependencies = [ - "once_cell", - "rustc-hash", - "serde", - "string_cache", - "string_cache_codegen", - "triomphe", -] - -[[package]] -name = "swc_common" -version = "0.32.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cb7fcd56655c8ae7dcf2344f0be6cbff4d9c7cb401fe3ec8e56e1de8dfe582" -dependencies = [ - "ast_node", - "better_scoped_tls", - "cfg-if 1.0.0", - "either", - "from_variant", - "new_debug_unreachable", - "num-bigint", - "once_cell", - "rustc-hash", - "serde", - "siphasher", - "sourcemap 6.4.1", - "string_cache", - "swc_atoms", - "swc_eq_ignore_macros", - "swc_visit", - "tracing", - "unicode-width", - "url", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "swc_config" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba1c7a40d38f9dd4e9a046975d3faf95af42937b34b2b963be4d8f01239584b" -dependencies = [ - "indexmap 1.9.3", - "serde", - "serde_json", - "swc_config_macro", -] - -[[package]] -name = "swc_config_macro" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b5aaca9a0082be4515f0fbbecc191bf5829cd25b5b9c0a2810f6a2bb0d6829" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.39", -] - -[[package]] -name = "swc_ecma_ast" -version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc2286cedd688a68f214faa1c19bb5cceab7c9c54d0cbe3273e4c1704e38f69" -dependencies = [ - "bitflags 2.4.1", - "is-macro", - "num-bigint", - "scoped-tls", - "serde", - "string_enum", - "swc_atoms", - "swc_common", - "unicode-id", -] - -[[package]] -name = "swc_ecma_codegen" -version = "0.144.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e62ba2c0ed1f119fc1a76542d007f1b2c12854d54dea15f5491363227debe11" -dependencies = [ - "memchr", - "num-bigint", - "once_cell", - "rustc-hash", - "serde", - "sourcemap 6.4.1", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_codegen_macros", - "tracing", -] - -[[package]] -name = "swc_ecma_codegen_macros" -version = "0.7.3" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdff076dccca6cc6a0e0b2a2c8acfb066014382bc6df98ec99e755484814384" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "pmutil", "proc-macro2", "quote", - "swc_macros_common", - "syn 2.0.39", -] - -[[package]] -name = "swc_ecma_loader" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d7c322462657ae27ac090a2c89f7e456c94416284a2f5ecf66c43a6a3c19d1" -dependencies = [ - "anyhow", - "pathdiff", - "serde", - "swc_common", - "tracing", -] - -[[package]] -name = "swc_ecma_parser" -version = "0.139.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab46cb863bc5cd61535464e07e5b74d5f792fa26a27b9f6fd4c8daca9903b7" -dependencies = [ - "either", - "num-bigint", - "num-traits", - "serde", - "smallvec", - "smartstring", - "stacker", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "tracing", - "typed-arena", -] - -[[package]] -name = "swc_ecma_transforms_base" -version = "0.132.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ffd4a8149052bfc1ec1832fcbe04f317846ce635a49ec438df33b06db27d26" -dependencies = [ - "better_scoped_tls", - "bitflags 2.4.1", - "indexmap 1.9.3", - "once_cell", - "phf", - "rustc-hash", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_utils", - "swc_ecma_visit", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_classes" -version = "0.121.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4b7fee0e2c6f12456d2aefb2418f2f26529b995945d493e1dce35a5a22584fc" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base", - "swc_ecma_utils", - "swc_ecma_visit", + "unicode-ident", ] [[package]] -name = "swc_ecma_transforms_macros" -version = "0.5.3" +name = "syn" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8188eab297da773836ef5cf2af03ee5cca7a563e1be4b146f8141452c28cc690" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ - "pmutil", "proc-macro2", "quote", - "swc_macros_common", - "syn 2.0.39", -] - -[[package]] -name = "swc_ecma_transforms_proposal" -version = "0.166.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122fd9a69f464694edefbf9c59106b3c15e5cc8cb8575a97836e4fb79018e98f" -dependencies = [ - "either", - "rustc-hash", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base", - "swc_ecma_transforms_classes", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_react" -version = "0.178.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675b5c755b0448268830e85e59429095d3423c0ce4a850b209c6f0eeab069f63" -dependencies = [ - "base64 0.13.1", - "dashmap", - "indexmap 1.9.3", - "once_cell", - "serde", - "sha-1", - "string_enum", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_transforms_base", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_typescript" -version = "0.182.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eba97b1ea71739fcf278aedad4677a3cacb52288a3f3566191b70d16a889de6" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base", - "swc_ecma_transforms_react", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_utils" -version = "0.122.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11006a3398ffd4693c4d3b0a1b1a5030edbdc04228159f5301120a6178144708" -dependencies = [ - "indexmap 1.9.3", - "num_cpus", - "once_cell", - "rustc-hash", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_visit", - "tracing", - "unicode-id", -] - -[[package]] -name = "swc_ecma_visit" -version = "0.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f628ec196e76e67892441e14eef2e423a738543d32bffdabfeec20c29582117" -dependencies = [ - "num-bigint", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_visit", - "tracing", + "unicode-ident", ] [[package]] -name = "swc_eq_ignore_macros" +name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a95d367e228d52484c53336991fdcf47b6b553ef835d9159db4ba40efb0ee8" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "swc_macros_common" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a273205ccb09b51fabe88c49f3b34c5a4631c4c00a16ae20e03111d6a42e832" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "swc_visit" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c337fbb2d191bf371173dea6a957f01899adb8f189c6c31b122a6cfc98fc3" -dependencies = [ - "either", - "swc_visit_macros", -] - -[[package]] -name = "swc_visit_macros" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f322730fb82f3930a450ac24de8c98523af7d34ab8cb2f46bcb405839891a99" -dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.39", -] +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "syn" -version = "1.0.109" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "futures-core", ] [[package]] -name = "syn" -version = "2.0.39" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.96", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -5542,26 +6198,27 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", - "xattr 1.0.1", + "xattr 1.4.0", ] [[package]] name = "tempfile" -version = "3.8.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fastrand", - "redox_syscall 0.4.1", - "rustix 0.38.21", - "windows-sys 0.48.0", + "getrandom 0.2.15", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -5577,55 +6234,72 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] -name = "text_lines" -version = "0.6.0" +name = "textwrap" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd5828de7deaa782e1dd713006ae96b3bee32d3279b79eb67ecf8072c059bcf" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ - "serde", + "smawk", + "unicode-linebreak", + "unicode-width 0.1.14", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "thingbuf" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "662b54ef6f7b4e71f683dadc787bbb2d8e8ef2f91b682ebed3164a5a7abca905" dependencies = [ - "unicode-width", + "parking_lot", + "pin-project", ] [[package]] -name = "textwrap" -version = "0.16.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] [[package]] name = "thiserror" -version = "1.0.50" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -5641,22 +6315,23 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -5671,10 +6346,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ + "num-conv", "time-core", ] @@ -5687,11 +6363,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -5704,22 +6390,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5734,13 +6419,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -5759,27 +6444,48 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.21", "tokio", ] [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -5803,30 +6509,41 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", "native-tls", "tokio", "tokio-native-tls", - "tungstenite", + "tungstenite 0.23.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", ] [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5843,21 +6560,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -5868,59 +6585,48 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.1.0", - "toml_datetime", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap 2.1.0", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.24", ] [[package]] name = "tonic" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", - "axum", - "base64 0.21.5", + "axum 0.6.20", + "base64 0.21.7", "bytes", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-timeout", "percent-encoding", "pin-project", "prost", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -5929,19 +6635,18 @@ dependencies = [ [[package]] name = "torut" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" +source = "git+https://github.com/Start9Labs/torut.git?branch=update%2Fdependencies#cc7a1425a01214465e106975e6690794d8551bdb" dependencies = [ - "base32", - "base64 0.13.1", + "base32 0.4.0", + "base64 0.21.7", "derive_more", "ed25519-dalek 1.0.1", "hex", - "hmac 0.11.0", + "hmac", "rand 0.7.3", "serde", "serde_derive", - "sha2 0.9.9", + "sha2 0.10.8", "sha3", "tokio", ] @@ -5966,23 +6671,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -5992,20 +6713,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -6013,9 +6734,9 @@ dependencies = [ [[package]] name = "tracing-error" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" dependencies = [ "tracing", "tracing-subscriber", @@ -6033,9 +6754,9 @@ dependencies = [ [[package]] name = "tracing-journald" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" +checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657" dependencies = [ "libc", "tracing-core", @@ -6055,9 +6776,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -6073,23 +6794,13 @@ dependencies = [ [[package]] name = "treediff" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5" dependencies = [ "serde_json", ] -[[package]] -name = "triomphe" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee8098afad3fb0c54a9007aab6804558410503ad676d4633f9c2559a00ac0f" -dependencies = [ - "serde", - "stable_deref_trait", -] - [[package]] name = "trust-dns-proto" version = "0.23.2" @@ -6097,7 +6808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" dependencies = [ "async-trait", - "cfg-if 1.0.0", + "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", @@ -6108,7 +6819,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "smallvec", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -6123,13 +6834,13 @@ checksum = "c540f73c2b2ec2f6c54eabd0900e7aafb747a820224b742f556e8faabb461bc7" dependencies = [ "async-trait", "bytes", - "cfg-if 1.0.0", + "cfg-if", "drain", "enum-as-inner", "futures-executor", "futures-util", "serde", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "toml 0.7.8", @@ -6139,54 +6850,98 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ts-rs" +version = "8.1.0" +source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature%2Ftop-level-as#7ae88ade90b5e724159048a663a0bdb04bed27f7" +dependencies = [ + "thiserror 1.0.69", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "8.1.0" +source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature%2Ftop-level-as#7ae88ade90b5e724159048a663a0bdb04bed27f7" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.96", + "termcolor", +] + +[[package]] +name = "tty-spawn" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "cb91489cf2611235ae8d755d66ab028437980ee573e2230c05af41b136236ad1" +dependencies = [ + "anyhow", + "nix 0.29.0", + "signal-hook", +] [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.2.0", "httparse", "log", "native-tls", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] [[package]] -name = "typed-arena" -version = "2.0.2" +name = "tungstenite" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] [[package]] name = "typed-builder" -version = "0.17.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c6a006a6d3d6a6f143fda41cf4d1ad35110080687628c9f2117bd3cc7924f3" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.17.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa054ee5e2346187d631d2f1d1fd3b33676772d6d03a2d84e1c5213b31674ee" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", ] [[package]] @@ -6195,6 +6950,23 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + [[package]] name = "unarray" version = "0.1.4" @@ -6203,57 +6975,66 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] -name = "unicode-id" -version = "0.3.4" +name = "unicode-ident" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "unicode-ident" -version = "1.0.12" +name = "unicode-linebreak" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_categories" @@ -6262,10 +7043,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] -name = "unsafe-libyaml" -version = "0.2.9" +name = "unix-named-pipe" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "6ad653da8f36ac5825ba06642b5a3cce14a4e52c6a5fab4a8928d53f4426dae2" +dependencies = [ + "errno 0.2.8", + "libc", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" @@ -6275,12 +7066,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -6298,31 +7089,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "uuid" -version = "1.5.0" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" -dependencies = [ - "getrandom 0.2.11", -] +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "v8" -version = "0.79.2" +name = "uuid" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15561535230812a1db89a696f1f16a12ae6c2c370c6b2241c68d4cb33963faf" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ - "bitflags 1.3.2", - "fslock", - "once_cell", - "which 4.4.2", + "getrandom 0.2.15", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -6330,17 +7127,11 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -6351,6 +7142,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -6372,48 +7173,56 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6421,28 +7230,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -6453,9 +7265,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -6463,20 +7285,17 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] -name = "which" -version = "3.1.1" +name = "webpki-roots" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ - "libc", + "rustls-pki-types", ] [[package]] @@ -6488,14 +7307,24 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.21", + "rustix", ] [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.8", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -6515,11 +7344,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -6530,20 +7359,41 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-targets 0.42.2", + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -6556,18 +7406,21 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.6", ] [[package]] @@ -6586,10 +7439,20 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] [[package]] name = "windows_aarch64_gnullvm" @@ -6598,10 +7461,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6610,10 +7473,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6622,10 +7485,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6634,10 +7503,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6646,10 +7515,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6658,10 +7527,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6669,11 +7538,26 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" -version = "0.5.19" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -6684,10 +7568,28 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "wyz" version = "0.5.1" @@ -6697,6 +7599,23 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xattr" version = "0.2.3" @@ -6708,34 +7627,44 @@ dependencies = [ [[package]] name = "xattr" -version = "1.0.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", + "linux-raw-sys", + "rustix", ] [[package]] -name = "yajrc" -version = "0.1.0" +name = "xdg-home" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40687b4c165cb760e35730055c8840f36897e7c98099b2d3d66ba8cb624c79a" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ - "anyhow", - "serde", - "serde_json", - "thiserror", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", ] [[package]] name = "yajrc" -version = "0.1.0" -source = "git+https://github.com/dr-bonez/yajrc.git?branch=develop#72a22f7ac2197d7a5cdce4be601cf20e5280eec5" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce7af47ad983c2f8357333ef87d859e66deb7eef4bf6f9e1ae7b5e99044a48bf" dependencies = [ "anyhow", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6744,37 +7673,155 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f355ab62ebe30b758c1f4ab096a306722c4b7dbfb9d8c07d18c70d71a945588" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "hashbrown 0.13.2", "lazy_static", "serde", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a0d989036cd60a1e91a54c9851fb9ad5bd96125d41803eed79d2e2ef74bd7" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.6.24", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3685b5c81fce630efc3e143a4ded235b107f1b1cdf186c3f115529e5e5ae4265" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.96", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519629a3f80976d89c575895b05677cbc45eaf9f70d62a364d819ba646409cc8" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.6.24", + "zvariant", +] + [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", + "synstructure", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -6787,5 +7834,140 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.96", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zip" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +dependencies = [ + "aes 0.8.4", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.7.0", + "lzma-rs", + "memchr", + "pbkdf2", + "rand 0.8.5", + "sha1", + "thiserror 2.0.11", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zvariant" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "winnow 0.6.24", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.96", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.96", + "winnow 0.6.24", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index 894362522..5b6823df2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,10 +1,3 @@ [workspace] -members = [ - "container-init", - "helpers", - "js-engine", - "models", - "snapshot-creator", - "startos", -] +members = ["helpers", "models", "startos"] diff --git a/core/README.md b/core/README.md index 7a4be62a1..76f4d4c86 100644 --- a/core/README.md +++ b/core/README.md @@ -8,9 +8,6 @@ ## Structure - `startos`: This contains the core library for StartOS that supports building `startbox`. -- `container-init` (ignore: deprecated) -- `js-engine`: This contains the library required to build `deno` to support running `.js` maintainer scripts for v0.3 -- `snapshot-creator`: This contains a binary used to build `v8` runtime snapshots, required for initializing `start-deno` - `helpers`: This contains utility functions used across both `startos` and `js-engine` - `models`: This contains types that are shared across `startos`, `js-engine`, and `helpers` @@ -24,8 +21,6 @@ several different names for different behaviour: `startd` and control it similarly to the UI - `start-sdk`: This is a CLI tool that aids in building and packaging services you wish to deploy to StartOS -- `start-deno`: This is a CLI tool invoked by startd to run `.js` maintainer scripts for v0.3 -- `avahi-alias`: This is a CLI tool invoked by startd to create aliases in `avahi` for mDNS ## Questions diff --git a/core/build-cli.sh b/core/build-cli.sh new file mode 100755 index 000000000..8e069a690 --- /dev/null +++ b/core/build-cli.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +if [ -z "$KERNEL_NAME" ]; then + KERNEL_NAME=$(uname -s) +fi + +if [ -z "$TARGET" ]; then + if [ "$KERNEL_NAME" = "Linux" ]; then + TARGET="$ARCH-unknown-linux-musl" + elif [ "$KERNEL_NAME" = "Darwin" ]; then + TARGET="$ARCH-apple-darwin" + else + >&2 echo "unknown kernel $KERNEL_NAME" + exit 1 + fi +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-zig-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/cargo-zigbuild' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-zig-builder sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,daemon,$FEATURES --locked --bin start-cli --target=$TARGET" +if [ "$(ls -nd core/target/$TARGET/release/start-cli | awk '{ print $3 }')" != "$UID" ]; then + rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi \ No newline at end of file diff --git a/core/build-containerbox.sh b/core/build-containerbox.sh new file mode 100755 index 000000000..e81efcc97 --- /dev/null +++ b/core/build-containerbox.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl" +if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/containerbox | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi \ No newline at end of file diff --git a/core/build-prod.sh b/core/build-prod.sh deleted file mode 100755 index 214429727..000000000 --- a/core/build-prod.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -cd "$(dirname "${BASH_SOURCE[0]}")" - -set -e -shopt -s expand_aliases - -if [ -z "$ARCH" ]; then - ARCH=$(uname -m) -fi - -USE_TTY= -if tty -s; then - USE_TTY="-it" -fi - -cd .. -FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" -RUSTFLAGS="" - -alias 'rust-gnu-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-arm-cross:aarch64' -alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src -P messense/rust-musl-cross:$ARCH-musl' - -set +e -fail= -echo "FEATURES=\"$FEATURES\"" -echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-gnu-builder sh -c "(cd core && cargo build --release --features avahi-alias,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-gnu)"; then - fail=true -fi -for ARCH in x86_64 aarch64 -do - if ! rust-musl-builder sh -c "(cd core && cargo build --release --locked --bin container-init)"; then - fail=true - fi -done -set -e -cd core - -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - -if [ -n "$fail" ]; then - exit 1 -fi diff --git a/core/build-registrybox.sh b/core/build-registrybox.sh new file mode 100755 index 000000000..3659b372a --- /dev/null +++ b/core/build-registrybox.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl" +if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/registrybox | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi diff --git a/core/build-startbox.sh b/core/build-startbox.sh new file mode 100755 index 000000000..9fad6fa3d --- /dev/null +++ b/core/build-startbox.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl" +if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/startbox | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi \ No newline at end of file diff --git a/core/build-ts.sh b/core/build-ts.sh new file mode 100755 index 000000000..c9890bfe7 --- /dev/null +++ b/core/build-ts.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "cd core && cargo test --release --features=test,$FEATURES 'export_bindings_' && chown \$UID:\$UID startos/bindings" +if [ "$(ls -nd core/startos/bindings | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID startos/bindings && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi \ No newline at end of file diff --git a/core/build-v8-snapshot.sh b/core/build-v8-snapshot.sh deleted file mode 100755 index 58ff27c79..000000000 --- a/core/build-v8-snapshot.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Reason for this being is that we need to create a snapshot for the deno runtime. It wants to pull 3 files from build, and during the creation it gets embedded, but for some -# reason during the actual runtime it is looking for them. So this will create a docker in arm that creates the snaphot needed for the arm - -cd "$(dirname "${BASH_SOURCE[0]}")" - -set -e -shopt -s expand_aliases - -if [ -z "$ARCH" ]; then - ARCH=$(uname -m) -fi - -USE_TTY= -if tty -s; then - USE_TTY="-it" -fi - -alias 'rust-gnu-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-arm-cross:aarch64' - -echo "Building " -cd .. -rust-gnu-builder sh -c "(cd core/ && cargo build -p snapshot_creator --release --target=${ARCH}-unknown-linux-gnu)" -cd - - -if [ "$ARCH" = "aarch64" ]; then - DOCKER_ARCH='arm64/v8' -elif [ "$ARCH" = "x86_64" ]; then - DOCKER_ARCH='amd64' -fi - -echo "Creating Arm v8 Snapshot" -docker run $USE_TTY --platform "linux/${DOCKER_ARCH}" --mount type=bind,src=$(pwd),dst=/mnt ubuntu:22.04 /bin/sh -c "cd /mnt && /mnt/target/${ARCH}-unknown-linux-gnu/release/snapshot_creator" -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo -sudo chown $USER JS_SNAPSHOT.bin -sudo chmod 0644 JS_SNAPSHOT.bin - -sudo mv -f JS_SNAPSHOT.bin ./js-engine/src/artifacts/JS_SNAPSHOT.${ARCH}.bin \ No newline at end of file diff --git a/core/container-init/Cargo.toml b/core/container-init/Cargo.toml deleted file mode 100644 index 8229973d7..000000000 --- a/core/container-init/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "container-init" -version = "0.1.0" -edition = "2021" -rust = "1.66" - -[features] -dev = [] -metal = [] -sound = [] -unstable = [] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -async-stream = "0.3" -# cgroups-rs = "0.2" -color-eyre = "0.6" -futures = "0.3" -serde = { version = "1", features = ["derive", "rc"] } -serde_json = "1" -helpers = { path = "../helpers" } -imbl = "2" -nix = { version = "0.27", features = ["process", "signal"] } -tokio = { version = "1", features = ["full"] } -tokio-stream = { version = "0.1", features = ["io-util", "sync", "net"] } -tracing = "0.1" -tracing-error = "0.2" -tracing-futures = "0.2" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -yajrc = { version = "*", git = "https://github.com/dr-bonez/yajrc.git", branch = "develop" } - -[target.'cfg(target_os = "linux")'.dependencies] -procfs = "0.15" - -[profile.test] -opt-level = 3 - -[profile.dev.package.backtrace] -opt-level = 3 diff --git a/core/container-init/src/lib.rs b/core/container-init/src/lib.rs deleted file mode 100644 index 63d3380a7..000000000 --- a/core/container-init/src/lib.rs +++ /dev/null @@ -1,214 +0,0 @@ -use nix::unistd::Pid; -use serde::{Deserialize, Serialize, Serializer}; -use yajrc::RpcMethod; - -/// Know what the process is called -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProcessId(pub u32); -impl From for Pid { - fn from(pid: ProcessId) -> Self { - Pid::from_raw(pid.0 as i32) - } -} -impl From for ProcessId { - fn from(pid: Pid) -> Self { - ProcessId(pid.as_raw() as u32) - } -} -impl From for ProcessId { - fn from(pid: i32) -> Self { - ProcessId(pid as u32) - } -} - -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProcessGroupId(pub u32); - -#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum OutputStrategy { - Inherit, - Collect, -} - -#[derive(Debug, Clone, Copy)] -pub struct RunCommand; -impl Serialize for RunCommand { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RunCommandParams { - pub gid: Option, - pub command: String, - pub args: Vec, - pub output: OutputStrategy, -} -impl RpcMethod for RunCommand { - type Params = RunCommandParams; - type Response = ProcessId; - fn as_str<'a>(&'a self) -> &'a str { - "command" - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum LogLevel { - Trace(String), - Warn(String), - Error(String), - Info(String), - Debug(String), -} -impl LogLevel { - pub fn trace(&self) { - match self { - LogLevel::Trace(x) => tracing::trace!("{}", x), - LogLevel::Warn(x) => tracing::warn!("{}", x), - LogLevel::Error(x) => tracing::error!("{}", x), - LogLevel::Info(x) => tracing::info!("{}", x), - LogLevel::Debug(x) => tracing::debug!("{}", x), - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Log; -impl Serialize for Log { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LogParams { - pub gid: Option, - pub level: LogLevel, -} -impl RpcMethod for Log { - type Params = LogParams; - type Response = (); - fn as_str<'a>(&'a self) -> &'a str { - "log" - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ReadLineStdout; -impl Serialize for ReadLineStdout { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReadLineStdoutParams { - pub pid: ProcessId, -} -impl RpcMethod for ReadLineStdout { - type Params = ReadLineStdoutParams; - type Response = String; - fn as_str<'a>(&'a self) -> &'a str { - "read-line-stdout" - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ReadLineStderr; -impl Serialize for ReadLineStderr { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReadLineStderrParams { - pub pid: ProcessId, -} -impl RpcMethod for ReadLineStderr { - type Params = ReadLineStderrParams; - type Response = String; - fn as_str<'a>(&'a self) -> &'a str { - "read-line-stderr" - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Output; -impl Serialize for Output { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OutputParams { - pub pid: ProcessId, -} -impl RpcMethod for Output { - type Params = OutputParams; - type Response = String; - fn as_str<'a>(&'a self) -> &'a str { - "output" - } -} - -#[derive(Debug, Clone, Copy)] -pub struct SendSignal; -impl Serialize for SendSignal { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SendSignalParams { - pub pid: ProcessId, - pub signal: u32, -} -impl RpcMethod for SendSignal { - type Params = SendSignalParams; - type Response = (); - fn as_str<'a>(&'a self) -> &'a str { - "signal" - } -} - -#[derive(Debug, Clone, Copy)] -pub struct SignalGroup; -impl Serialize for SignalGroup { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Serialize::serialize(Self.as_str(), serializer) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignalGroupParams { - pub gid: ProcessGroupId, - pub signal: u32, -} -impl RpcMethod for SignalGroup { - type Params = SignalGroupParams; - type Response = (); - fn as_str<'a>(&'a self) -> &'a str { - "signal-group" - } -} diff --git a/core/container-init/src/main.rs b/core/container-init/src/main.rs deleted file mode 100644 index 997537808..000000000 --- a/core/container-init/src/main.rs +++ /dev/null @@ -1,428 +0,0 @@ -use std::collections::BTreeMap; -use std::ops::DerefMut; -use std::os::unix::process::ExitStatusExt; -use std::process::Stdio; -use std::sync::Arc; - -use container_init::{ - LogParams, OutputParams, OutputStrategy, ProcessGroupId, ProcessId, RunCommandParams, - SendSignalParams, SignalGroupParams, -}; -use futures::StreamExt; -use helpers::NonDetachingJoinHandle; -use nix::errno::Errno; -use nix::sys::signal::Signal; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::process::{Child, Command}; -use tokio::select; -use tokio::sync::{watch, Mutex}; -use yajrc::{Id, RpcError}; - -/// Outputs embedded in the JSONRpc output of the executable. -#[derive(Debug, Clone, Serialize)] -#[serde(untagged)] -enum Output { - Command(ProcessId), - ReadLineStdout(String), - ReadLineStderr(String), - Output(String), - Log, - Signal, - SignalGroup, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "method", content = "params", rename_all = "kebab-case")] -enum Input { - /// Run a new command, with the args - Command(RunCommandParams), - /// Want to log locall on the service rather than the eos - Log(LogParams), - // /// Get a line of stdout from the command - // ReadLineStdout(ReadLineStdoutParams), - // /// Get a line of stderr from the command - // ReadLineStderr(ReadLineStderrParams), - /// Get output of command - Output(OutputParams), - /// Send the sigterm to the process - Signal(SendSignalParams), - /// Signal a group of processes - SignalGroup(SignalGroupParams), -} - -#[derive(Deserialize)] -struct IncomingRpc { - id: Id, - #[serde(flatten)] - input: Input, -} - -struct ChildInfo { - gid: Option, - child: Arc>>, - output: Option, -} - -struct InheritOutput { - _thread: NonDetachingJoinHandle<()>, - stdout: watch::Receiver, - stderr: watch::Receiver, -} - -struct HandlerMut { - processes: BTreeMap, - // groups: BTreeMap, -} - -#[derive(Clone)] -struct Handler { - children: Arc>, -} -impl Handler { - fn new() -> Self { - Handler { - children: Arc::new(Mutex::new(HandlerMut { - processes: BTreeMap::new(), - // groups: BTreeMap::new(), - })), - } - } - async fn handle(&self, req: Input) -> Result { - Ok(match req { - Input::Command(RunCommandParams { - gid, - command, - args, - output, - }) => Output::Command(self.command(gid, command, args, output).await?), - // Input::ReadLineStdout(ReadLineStdoutParams { pid }) => { - // Output::ReadLineStdout(self.read_line_stdout(pid).await?) - // } - // Input::ReadLineStderr(ReadLineStderrParams { pid }) => { - // Output::ReadLineStderr(self.read_line_stderr(pid).await?) - // } - Input::Log(LogParams { gid: _, level }) => { - level.trace(); - Output::Log - } - Input::Output(OutputParams { pid }) => Output::Output(self.output(pid).await?), - Input::Signal(SendSignalParams { pid, signal }) => { - self.signal(pid, signal).await?; - Output::Signal - } - Input::SignalGroup(SignalGroupParams { gid, signal }) => { - self.signal_group(gid, signal).await?; - Output::SignalGroup - } - }) - } - - async fn command( - &self, - gid: Option, - command: String, - args: Vec, - output: OutputStrategy, - ) -> Result { - let mut cmd = Command::new(command); - cmd.args(args); - cmd.kill_on_drop(true); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - let mut child = cmd.spawn().map_err(|e| { - let mut err = yajrc::INTERNAL_ERROR.clone(); - err.data = Some(json!(e.to_string())); - err - })?; - let pid = ProcessId(child.id().ok_or_else(|| { - let mut err = yajrc::INTERNAL_ERROR.clone(); - err.data = Some(json!("Child has no pid")); - err - })?); - let output = match output { - OutputStrategy::Inherit => { - let (stdout_send, stdout) = watch::channel(String::new()); - let (stderr_send, stderr) = watch::channel(String::new()); - if let (Some(child_stdout), Some(child_stderr)) = - (child.stdout.take(), child.stderr.take()) - { - Some(InheritOutput { - _thread: tokio::spawn(async move { - tokio::join!( - async { - if let Err(e) = async { - let mut lines = BufReader::new(child_stdout).lines(); - while let Some(line) = lines.next_line().await? { - tracing::info!("({}): {}", pid.0, line); - let _ = stdout_send.send(line); - } - Ok::<_, std::io::Error>(()) - } - .await - { - tracing::error!( - "Error reading stdout of pid {}: {}", - pid.0, - e - ); - } - }, - async { - if let Err(e) = async { - let mut lines = BufReader::new(child_stderr).lines(); - while let Some(line) = lines.next_line().await? { - tracing::warn!("({}): {}", pid.0, line); - let _ = stderr_send.send(line); - } - Ok::<_, std::io::Error>(()) - } - .await - { - tracing::error!( - "Error reading stdout of pid {}: {}", - pid.0, - e - ); - } - } - ); - }) - .into(), - stdout, - stderr, - }) - } else { - None - } - } - OutputStrategy::Collect => None, - }; - self.children.lock().await.processes.insert( - pid, - ChildInfo { - gid, - child: Arc::new(Mutex::new(Some(child))), - output, - }, - ); - Ok(pid) - } - - async fn output(&self, pid: ProcessId) -> Result { - let not_found = || { - let mut err = yajrc::INTERNAL_ERROR.clone(); - err.data = Some(json!(format!("Child with pid {} not found", pid.0))); - err - }; - let mut child = { - self.children - .lock() - .await - .processes - .get(&pid) - .ok_or_else(not_found)? - .child - .clone() - } - .lock_owned() - .await; - if let Some(child) = child.take() { - let output = child.wait_with_output().await?; - if output.status.success() { - Ok(String::from_utf8(output.stdout).map_err(|_| yajrc::PARSE_ERROR)?) - } else { - Err(RpcError { - code: output - .status - .code() - .or_else(|| output.status.signal().map(|s| 128 + s)) - .unwrap_or(0), - message: "Command failed".into(), - data: Some(json!(String::from_utf8(if output.stderr.is_empty() { - output.stdout - } else { - output.stderr - }) - .map_err(|_| yajrc::PARSE_ERROR)?)), - }) - } - } else { - Err(not_found()) - } - } - - async fn signal(&self, pid: ProcessId, signal: u32) -> Result<(), RpcError> { - let not_found = || { - let mut err = yajrc::INTERNAL_ERROR.clone(); - err.data = Some(json!(format!("Child with pid {} not found", pid.0))); - err - }; - - Self::killall(pid, Signal::try_from(signal as i32)?)?; - - if signal == 9 { - self.children - .lock() - .await - .processes - .remove(&pid) - .ok_or_else(not_found)?; - } - Ok(()) - } - - async fn signal_group(&self, gid: ProcessGroupId, signal: u32) -> Result<(), RpcError> { - let mut to_kill = Vec::new(); - { - let mut children_ref = self.children.lock().await; - let children = std::mem::take(&mut children_ref.deref_mut().processes); - for (pid, child_info) in children { - if child_info.gid == Some(gid) { - to_kill.push(pid); - } else { - children_ref.processes.insert(pid, child_info); - } - } - } - for pid in to_kill { - tracing::info!("Killing pid {}", pid.0); - Self::killall(pid, Signal::try_from(signal as i32)?)?; - } - - Ok(()) - } - - fn killall(pid: ProcessId, signal: Signal) -> Result<(), RpcError> { - for proc in procfs::process::all_processes()? { - let stat = proc?.stat()?; - if ProcessId::from(stat.ppid) == pid { - Self::killall(stat.pid.into(), signal)?; - } - } - if let Err(e) = nix::sys::signal::kill(pid.into(), Some(signal)) { - if e != Errno::ESRCH { - tracing::error!("Failed to kill pid {}: {}", pid.0, e); - } - } - Ok(()) - } - - async fn graceful_exit(self) { - let kill_all = futures::stream::iter( - std::mem::take(&mut self.children.lock().await.deref_mut().processes).into_iter(), - ) - .for_each_concurrent(None, |(pid, child)| async move { - let _ = Self::killall(pid, Signal::SIGTERM); - if let Some(child) = child.child.lock().await.take() { - let _ = child.wait_with_output().await; - } - }); - kill_all.await - } -} - -#[tokio::main] -async fn main() { - use tokio::signal::unix::{signal, SignalKind}; - let mut sigint = signal(SignalKind::interrupt()).unwrap(); - let mut sigterm = signal(SignalKind::terminate()).unwrap(); - let mut sigquit = signal(SignalKind::quit()).unwrap(); - let mut sighangup = signal(SignalKind::hangup()).unwrap(); - - use tracing_error::ErrorLayer; - use tracing_subscriber::prelude::*; - use tracing_subscriber::{fmt, EnvFilter}; - - let filter_layer = EnvFilter::new("container_init=debug"); - let fmt_layer = fmt::layer().with_target(true); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::default()) - .init(); - color_eyre::install().unwrap(); - - let handler = Handler::new(); - let handler_thread = async { - let listener = tokio::net::UnixListener::bind("/start9/sockets/rpc.sock")?; - loop { - let (stream, _) = listener.accept().await?; - let (r, w) = stream.into_split(); - let mut lines = BufReader::new(r).lines(); - let handler = handler.clone(); - tokio::spawn(async move { - let w = Arc::new(Mutex::new(w)); - while let Some(line) = lines.next_line().await.transpose() { - let handler = handler.clone(); - let w = w.clone(); - tokio::spawn(async move { - if let Err(e) = async { - let req = serde_json::from_str::(&line?)?; - match handler.handle(req.input).await { - Ok(output) => { - if w.lock().await.write_all( - format!("{}\n", json!({ "id": req.id, "jsonrpc": "2.0", "result": output })) - .as_bytes(), - ) - .await.is_err() { - tracing::error!("Error sending to {id:?}", id = req.id); - } - } - Err(e) => - if w - .lock() - .await - .write_all( - format!("{}\n", json!({ "id": req.id, "jsonrpc": "2.0", "error": e })) - .as_bytes(), - ) - .await.is_err() { - - tracing::error!("Handle + Error sending to {id:?}", id = req.id); - }, - } - Ok::<_, color_eyre::Report>(()) - } - .await - { - tracing::error!("Error parsing RPC request: {}", e); - tracing::debug!("{:?}", e); - } - }); - } - Ok::<_, std::io::Error>(()) - }); - } - #[allow(unreachable_code)] - Ok::<_, std::io::Error>(()) - }; - - select! { - res = handler_thread => { - match res { - Ok(()) => tracing::debug!("Done with inputs/outputs"), - Err(e) => { - tracing::error!("Error reading RPC input: {}", e); - tracing::debug!("{:?}", e); - } - } - }, - _ = sigint.recv() => { - tracing::debug!("SIGINT"); - }, - _ = sigterm.recv() => { - tracing::debug!("SIGTERM"); - }, - _ = sigquit.recv() => { - tracing::debug!("SIGQUIT"); - }, - _ = sighangup.recv() => { - tracing::debug!("SIGHUP"); - } - } - handler.graceful_exit().await; - ::std::process::exit(0) -} diff --git a/core/helpers/Cargo.toml b/core/helpers/Cargo.toml index 83e1fd788..caf4b3eef 100644 --- a/core/helpers/Cargo.toml +++ b/core/helpers/Cargo.toml @@ -11,9 +11,9 @@ futures = "0.3.28" lazy_async_pool = "0.3.3" models = { path = "../models" } pin-project = "1.1.3" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1.14", features = ["io-util", "sync"] } tracing = "0.1.39" -yajrc = { version = "*", git = "https://github.com/dr-bonez/yajrc.git", branch = "develop" } diff --git a/core/helpers/src/lib.rs b/core/helpers/src/lib.rs index 226787590..a9df58ece 100644 --- a/core/helpers/src/lib.rs +++ b/core/helpers/src/lib.rs @@ -6,16 +6,15 @@ use std::time::Duration; use color_eyre::eyre::{eyre, Context, Error}; use futures::future::BoxFuture; use futures::FutureExt; +use models::ResultExt; use tokio::fs::File; use tokio::sync::oneshot; use tokio::task::{JoinError, JoinHandle, LocalSet}; mod byte_replacement_reader; -mod rpc_client; mod rsync; mod script_dir; pub use byte_replacement_reader::*; -pub use rpc_client::{RpcClient, UnixRpcClient}; pub use rsync::*; pub use script_dir::*; @@ -52,7 +51,8 @@ pub async fn canonicalize( } let path = path.as_ref(); if tokio::fs::metadata(path).await.is_err() { - if let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) { + let parent = path.parent().unwrap_or(Path::new(".")); + if let Some(file_name) = path.file_name() { if create_parent && tokio::fs::metadata(parent).await.is_err() { return Ok(create_canonical_folder(parent).await?.join(file_name)); } else { @@ -177,7 +177,7 @@ impl Drop for AtomicFile { if let Some(file) = self.file.take() { drop(file); let path = std::mem::take(&mut self.tmp_path); - tokio::spawn(async move { tokio::fs::remove_file(path).await.unwrap() }); + tokio::spawn(async move { tokio::fs::remove_file(path).await.log_err() }); } } } diff --git a/core/helpers/src/script_dir.rs b/core/helpers/src/script_dir.rs index d90051899..5cedd419f 100644 --- a/core/helpers/src/script_dir.rs +++ b/core/helpers/src/script_dir.rs @@ -1,10 +1,14 @@ use std::path::{Path, PathBuf}; -use models::{PackageId, Version}; +use models::{PackageId, VersionString}; pub const PKG_SCRIPT_DIR: &str = "package-data/scripts"; -pub fn script_dir>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf { +pub fn script_dir>( + datadir: P, + pkg_id: &PackageId, + version: &VersionString, +) -> PathBuf { datadir .as_ref() .join(&*PKG_SCRIPT_DIR) diff --git a/core/install-sdk.sh b/core/install-cli.sh similarity index 52% rename from core/install-sdk.sh rename to core/install-cli.sh index 4b316ccd7..b278947a3 100755 --- a/core/install-sdk.sh +++ b/core/install-cli.sh @@ -2,17 +2,18 @@ cd "$(dirname "${BASH_SOURCE[0]}")" -set -e +set -ea shopt -s expand_aliases web="../web/dist/static" [ -d "$web" ] || mkdir -p "$web" if [ -z "$PLATFORM" ]; then - export PLATFORM=$(uname -m) + PLATFORM=$(uname -m) fi -cargo install --path=./startos --no-default-features --features=js-engine,sdk,cli --locked -startbox_loc=$(which startbox) -ln -sf $startbox_loc $(dirname $startbox_loc)/start-cli -ln -sf $startbox_loc $(dirname $startbox_loc)/start-sdk +if [ "$PLATFORM" = "arm64" ]; then + PLATFORM="aarch64" +fi + +cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked diff --git a/core/js-engine/Cargo.toml b/core/js-engine/Cargo.toml deleted file mode 100644 index 14205109b..000000000 --- a/core/js-engine/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "js-engine" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-trait = "0.1.74" -dashmap = "5.5.3" -deno_core = "=0.222.0" -deno_ast = { version = "=0.29.5", features = ["transpiling"] } -container-init = { path = "../container-init" } -reqwest = { version = "0.11.22" } -sha2 = "0.10.8" -itertools = "0.11.0" -lazy_static = "1.4.0" -models = { path = "../models" } -helpers = { path = "../helpers" } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_json = "1.0" -tokio = { version = "1", features = ["full"] } -tracing = "0.1" diff --git a/core/js-engine/src/artifacts/JS_SNAPSHOT.aarch64.bin b/core/js-engine/src/artifacts/JS_SNAPSHOT.aarch64.bin deleted file mode 100644 index 305aa2d4c..000000000 Binary files a/core/js-engine/src/artifacts/JS_SNAPSHOT.aarch64.bin and /dev/null differ diff --git a/core/js-engine/src/artifacts/JS_SNAPSHOT.x86_64.bin b/core/js-engine/src/artifacts/JS_SNAPSHOT.x86_64.bin deleted file mode 100644 index 7f7d10689..000000000 Binary files a/core/js-engine/src/artifacts/JS_SNAPSHOT.x86_64.bin and /dev/null differ diff --git a/core/js-engine/src/artifacts/loadModule.js b/core/js-engine/src/artifacts/loadModule.js deleted file mode 100644 index de30eac89..000000000 --- a/core/js-engine/src/artifacts/loadModule.js +++ /dev/null @@ -1,242 +0,0 @@ -import Deno from "/deno_global.js"; -import * as mainModule from "/embassy.js"; - -function requireParam(param) { - throw new Error(`Missing required parameter ${param}`); -} - -/** - * This is using the simplified json pointer spec, using no escapes and arrays - * @param {object} obj - * @param {string} pointer - * @returns - */ -function jsonPointerValue(obj, pointer) { - const paths = pointer.substring(1).split("/"); - for (const path of paths) { - if (obj == null) { - return null; - } - obj = (obj || {})[path]; - } - return obj; -} - -function maybeDate(value) { - if (!value) return value; - return new Date(value); -} -const writeFile = ( - { - path = requireParam("path"), - volumeId = requireParam("volumeId"), - toWrite = requireParam("toWrite"), - } = requireParam("options"), -) => Deno.core.opAsync("write_file", volumeId, path, toWrite); - -const readFile = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => Deno.core.opAsync("read_file", volumeId, path); - - - -const runDaemon = ( - { command = requireParam("command"), args = [] } = requireParam("options"), -) => { - let id = Deno.core.opAsync("start_command", command, args, "inherit", null); - let processId = id.then(x => x.processId) - let waitPromise = null; - return { - processId, - async wait() { - waitPromise = waitPromise || Deno.core.opAsync("wait_command", await processId) - return waitPromise - }, - async term(signal = 15) { - return Deno.core.opAsync("send_signal", await processId, 15) - } - } -}; -const runCommand = async ( - { command = requireParam("command"), args = [], timeoutMillis = 30000 } = requireParam("options"), -) => { - let id = Deno.core.opAsync("start_command", command, args, "collect", timeoutMillis); - let pid = id.then(x => x.processId) - return Deno.core.opAsync("wait_command", await pid) -}; -const signalGroup = async ( - { gid = requireParam("gid"), signal = requireParam("signal") } = requireParam("gid and signal") -) => { - return Deno.core.opAsync("signal_group", gid, signal); -}; -const sleep = (timeMs = requireParam("timeMs"), -) => Deno.core.opAsync("sleep", timeMs); - -const rename = ( - { - srcVolume = requireParam("srcVolume"), - dstVolume = requirePapram("dstVolume"), - srcPath = requireParam("srcPath"), - dstPath = requireParam("dstPath"), - } = requireParam("options"), -) => Deno.core.opAsync("rename", srcVolume, srcPath, dstVolume, dstPath); -const metadata = async ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => { - const data = await Deno.core.opAsync("metadata", volumeId, path); - return { - ...data, - modified: maybeDate(data.modified), - created: maybeDate(data.created), - accessed: maybeDate(data.accessed), - }; -}; -const removeFile = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => Deno.core.opAsync("remove_file", volumeId, path); -const isSandboxed = () => Deno.core.ops["is_sandboxed"](); - -const writeJsonFile = ( - { - volumeId = requireParam("volumeId"), - path = requireParam("path"), - toWrite = requireParam("toWrite"), - } = requireParam("options"), -) => - writeFile({ - volumeId, - path, - toWrite: JSON.stringify(toWrite), - }); - -const chown = async ( - { - volumeId = requireParam("volumeId"), - path = requireParam("path"), - uid = requireParam("uid"), - } = requireParam("options"), -) => { - return await Deno.core.opAsync("chown", volumeId, path, uid); -}; - -const chmod = async ( - { - volumeId = requireParam("volumeId"), - path = requireParam("path"), - mode = requireParam("mode"), - } = requireParam("options"), -) => { - return await Deno.core.opAsync("chmod", volumeId, path, mode); -}; -const readJsonFile = async ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => JSON.parse(await readFile({ volumeId, path })); -const createDir = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => Deno.core.opAsync("create_dir", volumeId, path); - -const readDir = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => Deno.core.opAsync("read_dir", volumeId, path); -const removeDir = ( - { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), -) => Deno.core.opAsync("remove_dir", volumeId, path); -const trace = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_trace", whatToTrace); -const warn = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_warn", whatToTrace); -const error = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_error", whatToTrace); -const debug = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_debug", whatToTrace); -const info = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_info", whatToTrace); -const fetch = async (url = requireParam ('url'), options = null) => { - const { body, ...response } = await Deno.core.opAsync("fetch", url, options); - const textValue = Promise.resolve(body); - return { - ...response, - text() { - return textValue; - }, - json() { - return textValue.then((x) => JSON.parse(x)); - }, - }; -}; - -const runRsync = ( - { - srcVolume = requireParam("srcVolume"), - dstVolume = requireParam("dstVolume"), - srcPath = requireParam("srcPath"), - dstPath = requireParam("dstPath"), - options = requireParam("options"), - } = requireParam("options"), -) => { - let id = Deno.core.opAsync("rsync", srcVolume, srcPath, dstVolume, dstPath, options); - let waitPromise = null; - return { - async id() { - return id - }, - async wait() { - waitPromise = waitPromise || Deno.core.opAsync("rsync_wait", await id) - return waitPromise - }, - async progress() { - return Deno.core.opAsync("rsync_progress", await id) - } - } -}; - -const diskUsage = async ({ - volumeId = requireParam("volumeId"), - path = requireParam("path"), -} = { volumeId: null, path: null }) => { - const [used, total] = await Deno.core.opAsync("disk_usage", volumeId, path); - return { used, total } -} - -const currentFunction = Deno.core.ops.current_function(); -const input = Deno.core.ops.get_input(); -const variable_args = Deno.core.ops.get_variable_args(); -const setState = (x) => Deno.core.ops.set_value(x); -const effects = { - chmod, - chown, - writeFile, - readFile, - writeJsonFile, - readJsonFile, - error, - warn, - debug, - trace, - info, - isSandboxed, - fetch, - removeFile, - createDir, - removeDir, - metadata, - rename, - runCommand, - sleep, - runDaemon, - signalGroup, - runRsync, - readDir, - diskUsage, -}; - -const defaults = { - "handleSignal": (effects, { gid, signal }) => { - return effects.signalGroup({ gid, signal }) - } -} - -const runFunction = jsonPointerValue(mainModule, currentFunction) || jsonPointerValue(defaults, currentFunction); -(async () => { - if (typeof runFunction !== "function") { - error(`Expecting ${currentFunction} to be a function`); - throw new Error(`Expecting ${currentFunction} to be a function`); - } - const answer = await runFunction(effects, input, ...variable_args); - setState(answer); -})(); diff --git a/core/js-engine/src/lib.rs b/core/js-engine/src/lib.rs deleted file mode 100644 index b0b9bea37..000000000 --- a/core/js-engine/src/lib.rs +++ /dev/null @@ -1,1219 +0,0 @@ -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::sync::Arc; -use std::time::SystemTime; - -use deno_core::anyhow::{anyhow, bail}; -use deno_core::error::AnyError; -use deno_core::{ - resolve_import, Extension, FastString, JsRuntime, ModuleLoader, ModuleSource, - ModuleSourceFuture, ModuleSpecifier, ModuleType, OpDecl, ResolutionKind, RuntimeOptions, - Snapshot, -}; -use helpers::{script_dir, spawn_local, Rsync}; -use models::{PackageId, ProcedureName, Version, VolumeId}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::io::AsyncReadExt; -use tokio::sync::Mutex; - -lazy_static::lazy_static! { - static ref DENO_GLOBAL_JS: ModuleSpecifier = "file:///deno_global.js".parse().unwrap(); - static ref LOAD_MODULE_JS: ModuleSpecifier = "file:///loadModule.js".parse().unwrap(); - static ref EMBASSY_JS: ModuleSpecifier = "file:///embassy.js".parse().unwrap(); -} - -pub trait PathForVolumeId: Send + Sync { - fn path_for( - &self, - data_dir: &Path, - package_id: &PackageId, - version: &Version, - volume_id: &VolumeId, - ) -> Option; - fn readonly(&self, volume_id: &VolumeId) -> bool; -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct JsCode(Arc); - -#[derive(Debug, Clone, Copy)] -pub enum JsError { - Unknown, - Javascript, - Engine, - BoundryLayerSerDe, - Tokio, - FileSystem, - Code(i32), - Timeout, - NotValidProcedureName, -} - -impl JsError { - pub fn as_code_num(&self) -> i32 { - match self { - JsError::Unknown => 1, - JsError::Javascript => 2, - JsError::Engine => 3, - JsError::BoundryLayerSerDe => 4, - JsError::Tokio => 5, - JsError::FileSystem => 6, - JsError::NotValidProcedureName => 7, - JsError::Code(code) => *code, - JsError::Timeout => 143, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MetadataJs { - file_type: String, - is_dir: bool, - is_file: bool, - is_symlink: bool, - len: u64, - modified: Option, - accessed: Option, - created: Option, - readonly: bool, - gid: u32, - mode: u32, - uid: u32, -} - -#[cfg(target_arch = "x86_64")] -const SNAPSHOT_BYTES: &[u8] = include_bytes!("./artifacts/JS_SNAPSHOT.x86_64.bin"); - -#[cfg(target_arch = "aarch64")] -const SNAPSHOT_BYTES: &[u8] = include_bytes!("./artifacts/JS_SNAPSHOT.aarch64.bin"); - -#[derive(Clone)] -struct JsContext { - sandboxed: bool, - datadir: PathBuf, - run_function: String, - version: Version, - package_id: PackageId, - volumes: Arc, - input: Value, - variable_args: Vec, - rsyncs: Arc)>>, -} -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -enum ResultType { - Error(String), - ErrorCode(i32, String), - Result(serde_json::Value), -} -#[derive(Clone, Default)] -struct AnswerState(std::sync::Arc>); - -#[derive(Clone, Debug)] -struct ModsLoader { - code: JsCode, -} - -impl ModuleLoader for ModsLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _is_main: ResolutionKind, - ) -> Result { - if referrer.contains("embassy") { - bail!("Embassy.js cannot import anything else"); - } - let s = resolve_import(specifier, referrer).unwrap(); - Ok(s) - } - - fn load( - &self, - module_specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - is_dyn_import: bool, - ) -> Pin> { - let module_specifier = module_specifier.as_str().to_owned(); - let module = match &*module_specifier { - "file:///deno_global.js" => Ok(ModuleSource::new( - ModuleType::JavaScript, - FastString::Static("const old_deno = Deno; Deno = null; export default old_deno"), - &DENO_GLOBAL_JS, - )), - "file:///loadModule.js" => Ok(ModuleSource::new( - ModuleType::JavaScript, - FastString::Static(include_str!("./artifacts/loadModule.js")), - &LOAD_MODULE_JS, - )), - "file:///embassy.js" => Ok(ModuleSource::new( - ModuleType::JavaScript, - self.code.0.clone().into(), - &EMBASSY_JS, - )), - - x => Err(anyhow!("Not allowed to import: {}", x)), - }; - let module = module.and_then(|m| { - if is_dyn_import { - bail!("Will not import dynamic"); - } - match &maybe_referrer { - Some(x) if x.as_str() == "file:///embassy.js" => { - bail!("StartJS is not allowed to import") - } - _ => (), - } - Ok(m) - }); - Box::pin(async move { module }) - } -} - -pub struct JsExecutionEnvironment { - sandboxed: bool, - base_directory: PathBuf, - module_loader: ModsLoader, - package_id: PackageId, - version: Version, - volumes: Arc, -} - -impl JsExecutionEnvironment { - pub async fn load_from_package( - data_directory: impl AsRef, - package_id: &PackageId, - version: &Version, - volumes: Box, - ) -> Result { - let data_dir = data_directory.as_ref(); - let base_directory = data_dir; - let js_code = JsCode({ - let file_path = script_dir(data_dir, package_id, version).join("embassy.js"); - let mut file = match tokio::fs::File::open(file_path.clone()).await { - Ok(x) => x, - Err(e) => { - tracing::debug!("path: {:?}", file_path); - tracing::debug!("{:?}", e); - return Err(( - JsError::FileSystem, - format!("The file opening '{:?}' created error: {}", file_path, e), - )); - } - }; - let mut buffer = Default::default(); - if let Err(err) = file.read_to_string(&mut buffer).await { - tracing::debug!("{:?}", err); - return Err(( - JsError::FileSystem, - format!("The file reading created error: {}", err), - )); - }; - buffer.into() - }); - Ok(JsExecutionEnvironment { - base_directory: base_directory.to_owned(), - module_loader: ModsLoader { code: js_code }, - package_id: package_id.clone(), - version: version.clone(), - volumes: volumes.into(), - sandboxed: false, - }) - } - pub fn read_only_effects(mut self) -> Self { - self.sandboxed = true; - self - } - - pub async fn run_action Deserialize<'de>>( - self, - procedure_name: ProcedureName, - input: Option, - variable_args: Vec, - ) -> Result { - let input = match serde_json::to_value(input) { - Ok(a) => a, - Err(err) => { - tracing::error!("{}", err); - tracing::debug!("{:?}", err); - return Err(( - JsError::BoundryLayerSerDe, - "Couldn't convert input".to_string(), - )); - } - }; - let safer_handle = spawn_local(|| self.execute(procedure_name, input, variable_args)).await; - let output = safer_handle.await.unwrap()?; - match serde_json::from_value(output.clone()) { - Ok(x) => Ok(x), - Err(err) => { - tracing::error!("{}", err); - tracing::debug!("{:?}", err); - Err(( - JsError::BoundryLayerSerDe, - format!( - "Couldn't convert output = {:#?} to the correct type", - serde_json::to_string_pretty(&output).unwrap_or_default() - ), - )) - } - } - } - fn declarations() -> Vec { - vec![ - fns::chown::decl(), - fns::chmod::decl(), - fns::fetch::decl(), - fns::read_file::decl(), - fns::metadata::decl(), - fns::write_file::decl(), - fns::rename::decl(), - fns::remove_file::decl(), - fns::create_dir::decl(), - fns::remove_dir::decl(), - fns::read_dir::decl(), - fns::disk_usage::decl(), - fns::current_function::decl(), - fns::log_trace::decl(), - fns::log_warn::decl(), - fns::log_error::decl(), - fns::log_debug::decl(), - fns::log_info::decl(), - fns::get_input::decl(), - fns::get_variable_args::decl(), - fns::set_value::decl(), - fns::is_sandboxed::decl(), - fns::sleep::decl(), - fns::rsync::decl(), - fns::rsync_wait::decl(), - fns::rsync_progress::decl(), - ] - } - - async fn execute( - self, - procedure_name: ProcedureName, - input: Value, - variable_args: Vec, - ) -> Result { - let base_directory = self.base_directory.clone(); - let answer_state = AnswerState::default(); - let ext_answer_state = answer_state.clone(); - let js_ctx = JsContext { - datadir: base_directory, - run_function: procedure_name - .js_function_name() - .map(Ok) - .unwrap_or_else(|| { - Err(( - JsError::NotValidProcedureName, - format!("procedure is not value: {:?}", procedure_name), - )) - })?, - package_id: self.package_id.clone(), - volumes: self.volumes.clone(), - version: self.version.clone(), - sandboxed: self.sandboxed, - input, - variable_args, - rsyncs: Default::default(), - }; - let ext = Extension::builder("embassy") - .ops(Self::declarations()) - .state(move |state| { - state.put(ext_answer_state.clone()); - state.put(js_ctx); - }) - .build(); - - let loader = std::rc::Rc::new(self.module_loader.clone()); - let runtime_options = RuntimeOptions { - module_loader: Some(loader), - extensions: vec![ext], - startup_snapshot: Some(Snapshot::Static(SNAPSHOT_BYTES)), - ..Default::default() - }; - let mut runtime = JsRuntime::new(runtime_options); - - let future = async move { - let mod_id = runtime - .load_main_module(&"file:///loadModule.js".parse().unwrap(), None) - .await?; - let evaluated = runtime.mod_evaluate(mod_id); - let res = runtime.run_event_loop(false).await; - res?; - evaluated.await??; - Ok::<_, AnyError>(()) - }; - - future.await.map_err(|e| { - tracing::debug!("{:?}", e); - (JsError::Javascript, format!("{}", e)) - })?; - - let answer = answer_state.0.lock().clone(); - Ok(answer) - } -} - -/// Note: Make sure that we have the assumption that all these methods are callable at any time, and all call restrictions should be in rust -mod fns { - use std::cell::RefCell; - use std::collections::BTreeMap; - use std::convert::TryFrom; - use std::fs::Permissions; - use std::os::unix::fs::MetadataExt; - use std::os::unix::prelude::PermissionsExt; - use std::path::{Path, PathBuf}; - use std::rc::Rc; - use std::time::Duration; - - use container_init::ProcessId; - use deno_core::anyhow::{anyhow, bail}; - use deno_core::error::AnyError; - use deno_core::*; - use helpers::{to_tmp_path, AtomicFile, Rsync, RsyncOptions}; - use itertools::Itertools; - use models::VolumeId; - use serde::{Deserialize, Serialize}; - use serde_json::Value; - use tokio::io::AsyncWriteExt; - use tokio::process::Command; - - use super::{AnswerState, JsContext}; - use crate::{system_time_as_unix_ms, MetadataJs}; - - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] - struct FetchOptions { - method: Option, - headers: Option>, - body: Option, - } - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] - struct FetchResponse { - method: String, - ok: bool, - status: u32, - headers: BTreeMap, - body: Option, - } - #[op] - async fn fetch( - state: Rc>, - url: url::Url, - options: Option, - ) -> Result { - let sandboxed = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.sandboxed - }; - - if sandboxed { - bail!("Will not run fetch in sandboxed mode"); - } - - let client = reqwest::Client::new(); - let options = options.unwrap_or_default(); - let method = options - .method - .unwrap_or_else(|| "GET".to_string()) - .to_uppercase(); - let mut request_builder = match &*method { - "GET" => client.get(url), - "POST" => client.post(url), - "PUT" => client.put(url), - "DELETE" => client.delete(url), - "HEAD" => client.head(url), - "PATCH" => client.patch(url), - x => bail!("Unsupported method: {}", x), - }; - if let Some(headers) = options.headers { - for (key, value) in headers { - request_builder = request_builder.header(key, value); - } - } - if let Some(body) = options.body { - request_builder = request_builder.body(body); - } - let response = request_builder.send().await?; - - let fetch_response = FetchResponse { - method, - ok: response.status().is_success(), - status: response.status().as_u16() as u32, - headers: response - .headers() - .iter() - .filter_map(|(head, value)| { - Some((format!("{}", head), value.to_str().ok()?.to_string())) - }) - .collect(), - body: response.text().await.ok(), - }; - - Ok(fetch_response) - } - - #[op] - async fn read_file( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result { - let volume_path = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))? - }; - //get_path_for in volume.rs - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - let answer = tokio::fs::read_to_string(new_file).await?; - Ok(answer) - } - #[op] - async fn metadata( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result { - let volume_path = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))? - }; - //get_path_for in volume.rs - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - let answer = tokio::fs::metadata(new_file).await?; - let metadata_js = MetadataJs { - file_type: format!("{:?}", answer.file_type()), - is_dir: answer.is_dir(), - is_file: answer.is_file(), - is_symlink: answer.is_symlink(), - len: answer.len(), - modified: answer - .modified() - .ok() - .as_ref() - .and_then(system_time_as_unix_ms), - accessed: answer - .accessed() - .ok() - .as_ref() - .and_then(system_time_as_unix_ms), - created: answer - .created() - .ok() - .as_ref() - .and_then(system_time_as_unix_ms), - readonly: answer.permissions().readonly(), - gid: answer.gid(), - mode: answer.mode(), - uid: answer.uid(), - }; - - Ok(metadata_js) - } - #[op] - async fn write_file( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - write: String, - ) -> Result<(), AnyError> { - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - let parent_new_file = new_file - .parent() - .ok_or_else(|| anyhow!("Expecting that file is not root"))?; - // With the volume check - if !is_subset(&volume_path, &parent_new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - let new_volume_tmp = to_tmp_path(&volume_path).map_err(|e| anyhow!("{}", e))?; - let hashed_name = { - use std::os::unix::ffi::OsStrExt; - - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - - hasher.update(path_in.as_os_str().as_bytes()); - let result = hasher.finalize(); - format!("{:X}", result) - }; - let temp_file = new_volume_tmp.join(&hashed_name); - let mut file = AtomicFile::new(&new_file, Some(&temp_file)) - .await - .map_err(|e| anyhow!("{}", e))?; - file.write_all(write.as_bytes()).await?; - file.save().await.map_err(|e| anyhow!("{}", e))?; - Ok(()) - } - #[op] - async fn rename( - state: Rc>, - src_volume: VolumeId, - src_path: PathBuf, - dst_volume: VolumeId, - dst_path: PathBuf, - ) -> Result<(), AnyError> { - let (volumes, volume_path, volume_path_out) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &src_volume) - .ok_or_else(|| anyhow!("There is no {} in volumes", src_volume))?; - let volume_path_out = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &dst_volume) - .ok_or_else(|| anyhow!("There is no {} in volumes", dst_volume))?; - (ctx.volumes.clone(), volume_path, volume_path_out) - }; - if volumes.readonly(&dst_volume) { - bail!("Volume {} is readonly", dst_volume); - } - - let src_path = src_path.strip_prefix("/").unwrap_or(&src_path); - let old_file = volume_path.join(src_path); - let parent_old_file = old_file - .parent() - .ok_or_else(|| anyhow!("Expecting that file is not root"))?; - // With the volume check - if !is_subset(&volume_path, &parent_old_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - old_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - - let dst_path = dst_path.strip_prefix("/").unwrap_or(&dst_path); - let new_file = volume_path_out.join(dst_path); - let parent_new_file = new_file - .parent() - .ok_or_else(|| anyhow!("Expecting that file is not root"))?; - // With the volume check - if !is_subset(&volume_path_out, &parent_new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path_out.to_string_lossy(), - ); - } - tokio::fs::rename(old_file, new_file).await?; - Ok(()) - } - - #[op] - async fn rsync( - state: Rc>, - src_volume: VolumeId, - src_path: PathBuf, - dst_volume: VolumeId, - dst_path: PathBuf, - options: RsyncOptions, - ) -> Result { - let (volumes, volume_path, volume_path_out, rsyncs) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &src_volume) - .ok_or_else(|| anyhow!("There is no {} in volumes", src_volume))?; - let volume_path_out = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &dst_volume) - .ok_or_else(|| anyhow!("There is no {} in volumes", dst_volume))?; - ( - ctx.volumes.clone(), - volume_path, - volume_path_out, - ctx.rsyncs.clone(), - ) - }; - if volumes.readonly(&dst_volume) { - bail!("Volume {} is readonly", dst_volume); - } - - let src_path = src_path.strip_prefix("/").unwrap_or(&src_path); - let src = volume_path.join(src_path); - // With the volume check - if !is_subset(&volume_path, &src).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - src.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - if tokio::fs::metadata(&src).await.is_err() { - bail!("Source at {} does not exists", src.to_string_lossy()); - } - - let dst_path = src_path.strip_prefix("/").unwrap_or(&dst_path); - let dst = volume_path_out.join(dst_path); - // With the volume check - if !is_subset(&volume_path_out, &dst).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - dst.to_string_lossy(), - volume_path_out.to_string_lossy(), - ); - } - - let running_rsync = Rsync::new(src, dst, options) - .await - .map_err(|e| anyhow::anyhow!("{:?}", e.source))?; - let insert_id = { - let mut rsyncs = rsyncs.lock().await; - let next = rsyncs.0 + 1; - rsyncs.0 = next; - rsyncs.1.insert(next, running_rsync); - next - }; - Ok(insert_id) - } - - #[op] - async fn rsync_wait(state: Rc>, id: usize) -> Result<(), AnyError> { - let rsyncs = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.rsyncs.clone() - }; - let running_rsync = match rsyncs.lock().await.1.remove(&id) { - Some(a) => a, - None => bail!("Couldn't find rsync at id {id}"), - }; - running_rsync - .wait() - .await - .map_err(|x| anyhow::anyhow!("{}", x.source))?; - Ok(()) - } - #[op] - async fn rsync_progress(state: Rc>, id: usize) -> Result { - use futures::StreamExt; - let rsyncs = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.rsyncs.clone() - }; - let mut running_rsync = match rsyncs.lock().await.1.remove(&id) { - Some(a) => a, - None => bail!("Couldn't find rsync at id {id}"), - }; - let progress = running_rsync.progress.next().await.unwrap_or_default(); - rsyncs.lock().await.1.insert(id, running_rsync); - Ok(progress) - } - #[op] - async fn remove_file( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result<(), AnyError> { - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - tokio::fs::remove_file(new_file).await?; - Ok(()) - } - #[op] - async fn remove_dir( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result<(), AnyError> { - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - tokio::fs::remove_dir_all(new_file).await?; - Ok(()) - } - #[op] - async fn create_dir( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result<(), AnyError> { - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - tokio::fs::create_dir_all(new_file).await?; - Ok(()) - } - #[op] - async fn read_dir( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ) -> Result, AnyError> { - let volume_path = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))? - }; - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - let mut reader = tokio::fs::read_dir(&new_file).await?; - let mut paths: Vec = Vec::new(); - let origin_path = format!("{}/", new_file.to_str().unwrap_or_default()); - let remove_new_file = |other_path: String| other_path.replacen(&origin_path, "", 1); - let has_origin_path = |other_path: &String| other_path.starts_with(&origin_path); - while let Some(entry) = reader.next_entry().await? { - entry - .path() - .to_str() - .into_iter() - .map(ToString::to_string) - .filter(&has_origin_path) - .map(&remove_new_file) - .for_each(|x| paths.push(x)); - } - paths.sort(); - Ok(paths) - } - - #[op] - async fn disk_usage( - state: Rc>, - volume_id: Option, - path_in: Option, - ) -> Result<(u64, u64), AnyError> { - let (base_path, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = if let Some(volume_id) = volume_id { - Some( - ctx.volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?, - ) - } else { - None - }; - (ctx.datadir.join("package-data"), volume_path) - }; - let path = if let (Some(volume_path), Some(path_in)) = (volume_path, path_in) { - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - Some(volume_path.join(path_in)) - } else { - None - }; - - if let Some(path) = path { - let size = String::from_utf8( - Command::new("df") - .arg("--output=size") - .arg("--block-size=1") - .arg(&base_path) - .stdout(std::process::Stdio::piped()) - .output() - .await? - .stdout, - )? - .lines() - .nth(1) - .unwrap_or_default() - .parse()?; - let used = String::from_utf8( - Command::new("du") - .arg("-s") - .arg("--block-size=1") - .arg(path) - .stdout(std::process::Stdio::piped()) - .output() - .await? - .stdout, - )? - .split_ascii_whitespace() - .next() - .unwrap_or_default() - .parse()?; - Ok((used, size)) - } else { - String::from_utf8( - Command::new("df") - .arg("--output=used,size") - .arg("--block-size=1") - .arg(&base_path) - .stdout(std::process::Stdio::piped()) - .output() - .await? - .stdout, - )? - .lines() - .nth(1) - .unwrap_or_default() - .split_ascii_whitespace() - .next_tuple() - .and_then(|(used, size)| Some((used.parse().ok()?, size.parse().ok()?))) - .ok_or_else(|| anyhow!("invalid output from df")) - } - } - - #[op] - fn current_function(state: &mut OpState) -> Result { - let ctx = state.borrow::(); - Ok(ctx.run_function.clone()) - } - - #[op] - async fn log_trace(state: Rc>, input: String) -> Result<(), AnyError> { - let ctx = { - let state = state.borrow(); - state.borrow::().clone() - }; - tracing::trace!( - package_id = tracing::field::display(&ctx.package_id), - run_function = tracing::field::display(&ctx.run_function), - "{}", - input - ); - Ok(()) - } - #[op] - async fn log_warn(state: Rc>, input: String) -> Result<(), AnyError> { - let ctx = { - let state = state.borrow(); - state.borrow::().clone() - }; - tracing::warn!( - package_id = tracing::field::display(&ctx.package_id), - run_function = tracing::field::display(&ctx.run_function), - "{}", - input - ); - Ok(()) - } - #[op] - async fn log_error(state: Rc>, input: String) -> Result<(), AnyError> { - let ctx = { - let state = state.borrow(); - state.borrow::().clone() - }; - tracing::error!( - package_id = tracing::field::display(&ctx.package_id), - run_function = tracing::field::display(&ctx.run_function), - "{}", - input - ); - Ok(()) - } - #[op] - async fn log_debug(state: Rc>, input: String) -> Result<(), AnyError> { - let ctx = { - let state = state.borrow(); - state.borrow::().clone() - }; - tracing::debug!( - package_id = tracing::field::display(&ctx.package_id), - run_function = tracing::field::display(&ctx.run_function), - "{}", - input - ); - Ok(()) - } - #[op] - async fn log_info(state: Rc>, input: String) -> Result<(), AnyError> { - let (package_id, run_function) = { - let state = state.borrow(); - let ctx: JsContext = state.borrow::().clone(); - (ctx.package_id, ctx.run_function) - }; - tracing::info!( - package_id = tracing::field::display(&package_id), - run_function = tracing::field::display(&run_function), - "{}", - input - ); - Ok(()) - } - - #[op] - fn get_input(state: &mut OpState) -> Result { - let ctx = state.borrow::(); - Ok(ctx.input.clone()) - } - #[op] - fn get_variable_args(state: &mut OpState) -> Result, AnyError> { - let ctx = state.borrow::(); - Ok(ctx.variable_args.clone()) - } - #[op] - fn set_value(state: &mut OpState, value: Value) -> Result<(), AnyError> { - let mut answer = state.borrow::().0.lock(); - *answer = value; - Ok(()) - } - #[op] - fn is_sandboxed(state: &mut OpState) -> Result { - let ctx = state.borrow::(); - Ok(ctx.sandboxed) - } - - #[derive(Debug, Clone, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct StartCommand { - process_id: ProcessId, - } - - #[op] - async fn sleep(time_ms: u64) -> Result<(), AnyError> { - tokio::time::sleep(Duration::from_millis(time_ms)).await; - - Ok(()) - } - - #[op] - async fn chown( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - ownership: u32, - ) -> Result<(), AnyError> { - let sandboxed = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.sandboxed - }; - - if sandboxed { - bail!("Will not run chown in sandboxed mode"); - } - - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - let output = tokio::process::Command::new("chown") - .arg("--recursive") - .arg(format!("{ownership}")) - .arg(new_file.as_os_str()) - .output() - .await?; - if !output.status.success() { - return Err(anyhow!("Chown Error")); - } - Ok(()) - } - #[op] - async fn chmod( - state: Rc>, - volume_id: VolumeId, - path_in: PathBuf, - mode: u32, - ) -> Result<(), AnyError> { - let sandboxed = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - ctx.sandboxed - }; - - if sandboxed { - bail!("Will not run chmod in sandboxed mode"); - } - - let (volumes, volume_path) = { - let state = state.borrow(); - let ctx: &JsContext = state.borrow(); - let volume_path = ctx - .volumes - .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) - .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; - (ctx.volumes.clone(), volume_path) - }; - if volumes.readonly(&volume_id) { - bail!("Volume {} is readonly", volume_id); - } - let path_in = path_in.strip_prefix("/").unwrap_or(&path_in); - let new_file = volume_path.join(path_in); - // With the volume check - if !is_subset(&volume_path, &new_file).await? { - bail!( - "Path '{}' has broken away from parent '{}'", - new_file.to_string_lossy(), - volume_path.to_string_lossy(), - ); - } - tokio::fs::set_permissions(new_file, Permissions::from_mode(mode)).await?; - Ok(()) - } - /// We need to make sure that during the file accessing, we don't reach beyond our scope of control - async fn is_subset( - parent: impl AsRef, - child: impl AsRef, - ) -> Result { - let child = { - let mut child_count = 0; - let mut child = child.as_ref(); - loop { - if child.ends_with("..") { - child_count += 1; - } else if child_count > 0 { - child_count -= 1; - } else { - let meta = tokio::fs::metadata(child).await; - if meta.is_ok() { - break; - } - } - child = match child.parent() { - Some(child) => child, - None => { - return Ok(false); - } - }; - } - tokio::fs::canonicalize(child).await? - }; - let parent = tokio::fs::canonicalize(parent).await?; - Ok(child.starts_with(parent)) - } - - #[tokio::test] - async fn test_is_subset() { - let home = std::env::var("HOME").unwrap(); - let home = Path::new(&home); - assert!(!is_subset(home, &home.join("code/fakedir/../../..")) - .await - .unwrap()) - } -} - -fn system_time_as_unix_ms(system_time: &SystemTime) -> Option { - system_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_millis() - .try_into() - .ok() -} diff --git a/core/models/Cargo.toml b/core/models/Cargo.toml index 9d75f92c4..9e216bc60 100644 --- a/core/models/Cargo.toml +++ b/core/models/Cargo.toml @@ -6,23 +6,26 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.7.5" base64 = "0.21.4" color-eyre = "0.6.2" ed25519-dalek = { version = "2.0.0", features = ["serde"] } lazy_static = "1.4" mbrman = "0.5.2" -emver = { version = "0.1", git = "https://github.com/Start9Labs/emver-rs.git", features = [ +exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ "serde", ] } ipnet = "2.8.0" +num_enum = "0.7.1" openssl = { version = "0.10.57", features = ["vendored"] } patch-db = { version = "*", path = "../../patch-db/patch-db", features = [ "trace", ] } rand = "0.8.5" regex = "1.10.2" -reqwest = "0.11.22" -rpc-toolkit = "0.2.2" +reqwest = "0.12" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" } +rustls = "0.23" serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" sqlx = { version = "0.7.2", features = [ @@ -31,8 +34,10 @@ sqlx = { version = "0.7.2", features = [ "postgres", ] } ssh-key = "0.6.2" +ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8" thiserror = "1.0" tokio = { version = "1", features = ["full"] } -torut = "0.2.1" +torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies" } tracing = "0.1.39" yasi = "0.1.5" +zbus = "5" diff --git a/core/models/bindings/ServiceInterfaceId.ts b/core/models/bindings/ServiceInterfaceId.ts new file mode 100644 index 000000000..87edd8694 --- /dev/null +++ b/core/models/bindings/ServiceInterfaceId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServiceInterfaceId = string; \ No newline at end of file diff --git a/core/models/src/clap.rs b/core/models/src/clap.rs new file mode 100644 index 000000000..122e8cf9d --- /dev/null +++ b/core/models/src/clap.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; +use std::str::FromStr; + +use rpc_toolkit::clap; +use rpc_toolkit::clap::builder::TypedValueParser; + +pub struct FromStrParser(PhantomData); +impl FromStrParser { + pub fn new() -> Self { + Self(PhantomData) + } +} +impl Clone for FromStrParser { + fn clone(&self) -> Self { + Self(PhantomData) + } +} +impl TypedValueParser for FromStrParser +where + T: FromStr + Clone + Send + Sync + 'static, + T::Err: std::fmt::Display, +{ + type Value = T; + fn parse_ref( + &self, + _: &clap::Command, + _: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + value + .to_string_lossy() + .parse() + .map_err(|e| clap::Error::raw(clap::error::ErrorKind::ValueValidation, e)) + } +} diff --git a/core/models/src/data_url.rs b/core/models/src/data_url.rs index e2141b15a..8a95c993e 100644 --- a/core/models/src/data_url.rs +++ b/core/models/src/data_url.rs @@ -6,11 +6,13 @@ use color_eyre::eyre::eyre; use reqwest::header::CONTENT_TYPE; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncRead, AsyncReadExt}; +use ts_rs::TS; use yasi::InternedString; use crate::{mime, Error, ErrorKind, ResultExt}; -#[derive(Clone)] +#[derive(Clone, TS)] +#[ts(type = "string")] pub struct DataUrl<'a> { mime: InternedString, data: Cow<'a, [u8]>, @@ -166,6 +168,6 @@ fn doesnt_reallocate() { mime: InternedString::intern("png"), data: Cow::Borrowed(&random[..i]), }; - assert_eq!(dbg!(icon.to_string()).capacity(), icon.data_url_len()); + assert_eq!(icon.to_string().capacity(), icon.data_url_len()); } } diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index f22624d36..21ff5072b 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -1,14 +1,20 @@ -use std::fmt::Display; +use std::fmt::{Debug, Display}; +use axum::http::uri::InvalidUri; +use axum::http::StatusCode; use color_eyre::eyre::eyre; +use num_enum::TryFromPrimitive; use patch_db::Revision; -use rpc_toolkit::hyper::http::uri::InvalidUri; use rpc_toolkit::reqwest; -use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::yajrc::{ + RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, +}; +use serde::{Deserialize, Serialize}; use crate::InvalidId; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)] +#[repr(i32)] pub enum ErrorKind { Unknown = 1, Filesystem = 2, @@ -39,7 +45,7 @@ pub enum ErrorKind { ConfigGen = 27, ParseNumber = 28, Database = 29, - InvalidPackageId = 30, + InvalidId = 30, InvalidSignature = 31, Backup = 32, Restore = 33, @@ -81,6 +87,10 @@ pub enum ErrorKind { CpuSettings = 69, Firmware = 70, Timeout = 71, + Lxc = 72, + Cancelled = 73, + Git = 74, + DBus = 75, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -115,7 +125,7 @@ impl ErrorKind { ConfigGen => "Config Generation Error", ParseNumber => "Number Parsing Error", Database => "Database Error", - InvalidPackageId => "Invalid Package ID", + InvalidId => "Invalid ID", InvalidSignature => "Invalid Signature", Backup => "Backup Error", Restore => "Restore Error", @@ -157,6 +167,10 @@ impl ErrorKind { CpuSettings => "CPU Settings Error", Firmware => "Firmware Error", Timeout => "Timeout Error", + Lxc => "LXC Error", + Cancelled => "Cancelled", + Git => "Git Error", + DBus => "DBus Error", } } } @@ -186,10 +200,33 @@ impl Error { revision: None, } } + pub fn clone_output(&self) -> Self { + Error { + source: ErrorData { + details: format!("{}", self.source), + debug: format!("{:?}", self.source), + } + .into(), + kind: self.kind, + revision: self.revision.clone(), + } + } +} +impl axum::response::IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let mut res = axum::Json(RpcError::from(self)).into_response(); + *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + res + } +} +impl From for Error { + fn from(value: std::convert::Infallible) -> Self { + match value {} + } } impl From for Error { fn from(err: InvalidId) -> Self { - Error::new(err, ErrorKind::InvalidPackageId) + Error::new(err, ErrorKind::InvalidId) } } impl From for Error { @@ -207,8 +244,8 @@ impl From for Error { Error::new(e, ErrorKind::Utf8) } } -impl From for Error { - fn from(e: emver::ParseError) -> Self { +impl From for Error { + fn from(e: exver::ParseError) -> Self { Error::new(e, ErrorKind::ParseVersion) } } @@ -287,6 +324,21 @@ impl From for Error { Error::new(e, kind) } } +impl From for Error { + fn from(e: torut::onion::OnionAddressParseError) -> Self { + Error::new(e, ErrorKind::Tor) + } +} +impl From for Error { + fn from(e: zbus::Error) -> Self { + Error::new(e, ErrorKind::DBus) + } +} +impl From for Error { + fn from(e: rustls::Error) -> Self { + Error::new(e, ErrorKind::OpenSsl) + } +} impl From for Error { fn from(value: patch_db::value::Error) -> Self { match value.kind { @@ -300,6 +352,61 @@ impl From for Error { } } +#[derive(Clone, Deserialize, Serialize)] +pub struct ErrorData { + pub details: String, + pub debug: String, +} +impl Display for ErrorData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.details, f) + } +} +impl Debug for ErrorData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.debug, f) + } +} +impl std::error::Error for ErrorData {} +impl From for ErrorData { + fn from(value: Error) -> Self { + Self { + details: value.to_string(), + debug: format!("{:?}", value), + } + } +} +impl From<&RpcError> for ErrorData { + fn from(value: &RpcError) -> Self { + Self { + details: value + .data + .as_ref() + .and_then(|d| { + d.as_object() + .and_then(|d| { + d.get("details") + .and_then(|d| d.as_str().map(|s| s.to_owned())) + }) + .or_else(|| d.as_str().map(|s| s.to_owned())) + }) + .unwrap_or_else(|| value.message.clone().into_owned()), + debug: value + .data + .as_ref() + .and_then(|d| { + d.as_object() + .and_then(|d| { + d.get("debug") + .and_then(|d| d.as_str().map(|s| s.to_owned())) + }) + .or_else(|| d.as_str().map(|s| s.to_owned())) + }) + .unwrap_or_else(|| value.message.clone().into_owned()), + } + } +} + impl From for RpcError { fn from(e: Error) -> Self { let mut data_object = serde_json::Map::with_capacity(3); @@ -318,10 +425,40 @@ impl From for RpcError { RpcError { code: e.kind as i32, message: e.kind.as_str().into(), - data: Some(data_object.into()), + data: Some( + match serde_json::to_value(&ErrorData { + details: format!("{}", e.source), + debug: format!("{:?}", e.source), + }) { + Ok(a) => a, + Err(e) => { + tracing::warn!("Error serializing revision for Error object: {}", e); + serde_json::Value::Null + } + }, + ), } } } +impl From for Error { + fn from(e: RpcError) -> Self { + Error::new( + ErrorData::from(&e), + if let Ok(kind) = e.code.try_into() { + kind + } else if e.code == METHOD_NOT_FOUND_ERROR.code { + ErrorKind::NotFound + } else if e.code == PARSE_ERROR.code + || e.code == INVALID_PARAMS_ERROR.code + || e.code == INVALID_REQUEST_ERROR.code + { + ErrorKind::Deserialization + } else { + ErrorKind::Unknown + }, + ) + } +} #[derive(Debug, Default)] pub struct ErrorCollection(Vec); @@ -377,10 +514,8 @@ where Self: Sized, { fn with_kind(self, kind: ErrorKind) -> Result; - fn with_ctx (ErrorKind, D), D: Display + Send + Sync + 'static>( - self, - f: F, - ) -> Result; + fn with_ctx (ErrorKind, D), D: Display>(self, f: F) -> Result; + fn log_err(self) -> Option; } impl ResultExt for Result where @@ -394,10 +529,7 @@ where }) } - fn with_ctx (ErrorKind, D), D: Display + Send + Sync + 'static>( - self, - f: F, - ) -> Result { + fn with_ctx (ErrorKind, D), D: Display>(self, f: F) -> Result { self.map_err(|e| { let (kind, ctx) = f(&e); let source = color_eyre::eyre::Error::from(e); @@ -410,6 +542,52 @@ where } }) } + + fn log_err(self) -> Option { + match self { + Ok(a) => Some(a), + Err(e) => { + let e: color_eyre::eyre::Error = e.into(); + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + None + } + } + } +} +impl ResultExt for Result { + fn with_kind(self, kind: ErrorKind) -> Result { + self.map_err(|e| Error { + source: e.source, + kind, + revision: e.revision, + }) + } + + fn with_ctx (ErrorKind, D), D: Display>(self, f: F) -> Result { + self.map_err(|e| { + let (kind, ctx) = f(&e); + let source = e.source; + let ctx = format!("{}: {}", ctx, source); + let source = source.wrap_err(ctx); + Error { + kind, + source, + revision: e.revision, + } + }) + } + + fn log_err(self) -> Option { + match self { + Ok(a) => Some(a), + Err(e) => { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + None + } + } + } } pub trait OptionExt diff --git a/core/models/src/id/action.rs b/core/models/src/id/action.rs index 9b814a98a..3f17048e2 100644 --- a/core/models/src/id/action.rs +++ b/core/models/src/id/action.rs @@ -2,10 +2,12 @@ use std::path::Path; use std::str::FromStr; use serde::{Deserialize, Serialize}; +use ts_rs::TS; use crate::{Id, InvalidId}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(type = "string")] pub struct ActionId(Id); impl FromStr for ActionId { type Err = InvalidId; diff --git a/core/models/src/id/health_check.rs b/core/models/src/id/health_check.rs index dc643c912..937f31aa3 100644 --- a/core/models/src/id/health_check.rs +++ b/core/models/src/id/health_check.rs @@ -1,16 +1,25 @@ use std::path::Path; +use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; -use crate::Id; +use crate::{Id, InvalidId}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(type = "string")] pub struct HealthCheckId(Id); impl std::fmt::Display for HealthCheckId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0) } } +impl FromStr for HealthCheckId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Id::from_str(s).map(HealthCheckId) + } +} impl AsRef for HealthCheckId { fn as_ref(&self) -> &str { self.0.as_ref() diff --git a/core/models/src/id/interface.rs b/core/models/src/id/host.rs similarity index 65% rename from core/models/src/id/interface.rs rename to core/models/src/id/host.rs index b9b32dd4a..2a1595bd8 100644 --- a/core/models/src/id/interface.rs +++ b/core/models/src/id/host.rs @@ -2,52 +2,65 @@ use std::path::Path; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; +use yasi::InternedString; use crate::{Id, InvalidId}; -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -pub struct InterfaceId(Id); -impl FromStr for InterfaceId { +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(type = "string")] +pub struct HostId(Id); +impl FromStr for HostId { type Err = InvalidId; fn from_str(s: &str) -> Result { Ok(Self(Id::try_from(s.to_owned())?)) } } -impl From for InterfaceId { +impl From for HostId { fn from(id: Id) -> Self { Self(id) } } -impl std::fmt::Display for InterfaceId { +impl From for Id { + fn from(value: HostId) -> Self { + value.0 + } +} +impl From for InternedString { + fn from(value: HostId) -> Self { + value.0.into() + } +} +impl std::fmt::Display for HostId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0) } } -impl std::ops::Deref for InterfaceId { +impl std::ops::Deref for HostId { type Target = str; fn deref(&self) -> &Self::Target { &*self.0 } } -impl AsRef for InterfaceId { +impl AsRef for HostId { fn as_ref(&self) -> &str { self.0.as_ref() } } -impl<'de> Deserialize<'de> for InterfaceId { +impl<'de> Deserialize<'de> for HostId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - Ok(InterfaceId(Deserialize::deserialize(deserializer)?)) + Ok(HostId(Deserialize::deserialize(deserializer)?)) } } -impl AsRef for InterfaceId { +impl AsRef for HostId { fn as_ref(&self) -> &Path { self.0.as_ref().as_ref() } } -impl<'q> sqlx::Encode<'q, sqlx::Postgres> for InterfaceId { +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for HostId { fn encode_by_ref( &self, buf: &mut >::ArgumentBuffer, @@ -55,7 +68,7 @@ impl<'q> sqlx::Encode<'q, sqlx::Postgres> for InterfaceId { <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&&**self, buf) } } -impl sqlx::Type for InterfaceId { +impl sqlx::Type for HostId { fn type_info() -> sqlx::postgres::PgTypeInfo { <&str as sqlx::Type>::type_info() } diff --git a/core/models/src/id/image.rs b/core/models/src/id/image.rs index 10ef0451d..69a04f880 100644 --- a/core/models/src/id/image.rs +++ b/core/models/src/id/image.rs @@ -1,19 +1,27 @@ use std::fmt::Debug; +use std::path::Path; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; -use crate::{Id, InvalidId, PackageId, Version}; +use crate::{Id, InvalidId, PackageId, VersionString}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(type = "string")] pub struct ImageId(Id); +impl AsRef for ImageId { + fn as_ref(&self) -> &Path { + self.0.as_ref().as_ref() + } +} impl std::fmt::Display for ImageId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0) } } impl ImageId { - pub fn for_package(&self, pkg_id: &PackageId, pkg_version: Option<&Version>) -> String { + pub fn for_package(&self, pkg_id: &PackageId, pkg_version: Option<&VersionString>) -> String { format!( "start9/{}/{}:{}", pkg_id, diff --git a/core/models/src/id/invalid_id.rs b/core/models/src/id/invalid_id.rs index d2cc82bd5..4a6f0f2e7 100644 --- a/core/models/src/id/invalid_id.rs +++ b/core/models/src/id/invalid_id.rs @@ -1,3 +1,5 @@ +use yasi::InternedString; + #[derive(Debug, thiserror::Error)] -#[error("Invalid ID")] -pub struct InvalidId; +#[error("Invalid ID: {0}")] +pub struct InvalidId(pub(super) InternedString); diff --git a/core/models/src/id/mod.rs b/core/models/src/id/mod.rs index ac32ceb22..0c313973f 100644 --- a/core/models/src/id/mod.rs +++ b/core/models/src/id/mod.rs @@ -1,41 +1,49 @@ use std::borrow::Borrow; +use std::str::FromStr; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use yasi::InternedString; mod action; -mod address; mod health_check; +mod host; mod image; -mod interface; mod invalid_id; mod package; +mod replay; +mod service_interface; mod volume; pub use action::ActionId; -pub use address::AddressId; pub use health_check::HealthCheckId; +pub use host::HostId; pub use image::ImageId; -pub use interface::InterfaceId; pub use invalid_id::InvalidId; pub use package::{PackageId, SYSTEM_PACKAGE_ID}; +pub use replay::ReplayId; +pub use service_interface::ServiceInterfaceId; pub use volume::VolumeId; lazy_static::lazy_static! { - static ref ID_REGEX: Regex = Regex::new("^[a-z]+(-[a-z]+)*$").unwrap(); + static ref ID_REGEX: Regex = Regex::new("^[a-z]+(-[a-z0-9]+)*$").unwrap(); pub static ref SYSTEM_ID: Id = Id(InternedString::intern("x_system")); } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Id(InternedString); +impl std::fmt::Debug for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} impl TryFrom for Id { type Error = InvalidId; fn try_from(value: InternedString) -> Result { - if ID_REGEX.is_match(&*value) { + if ID_REGEX.is_match(&value) { Ok(Id(value)) } else { - Err(InvalidId) + Err(InvalidId(value)) } } } @@ -45,24 +53,35 @@ impl TryFrom for Id { if ID_REGEX.is_match(&value) { Ok(Id(InternedString::intern(value))) } else { - Err(InvalidId) + Err(InvalidId(InternedString::intern(value))) } } } impl TryFrom<&str> for Id { type Error = InvalidId; fn try_from(value: &str) -> Result { - if ID_REGEX.is_match(&value) { + if ID_REGEX.is_match(value) { Ok(Id(InternedString::intern(value))) } else { - Err(InvalidId) + Err(InvalidId(InternedString::intern(value))) } } } +impl FromStr for Id { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} +impl From for InternedString { + fn from(value: Id) -> Self { + value.0 + } +} impl std::ops::Deref for Id { type Target = str; fn deref(&self) -> &Self::Target { - &*self.0 + &self.0 } } impl std::fmt::Display for Id { @@ -72,7 +91,7 @@ impl std::fmt::Display for Id { } impl AsRef for Id { fn as_ref(&self) -> &str { - &*self.0 + &self.0 } } impl Borrow for Id { @@ -94,7 +113,7 @@ impl Serialize for Id { where Ser: Serializer, { - serializer.serialize_str(&*self) + serializer.serialize_str(self) } } impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Id { diff --git a/core/models/src/id/package.rs b/core/models/src/id/package.rs index 14c29d88b..6e22b9d51 100644 --- a/core/models/src/id/package.rs +++ b/core/models/src/id/package.rs @@ -3,13 +3,16 @@ use std::path::Path; use std::str::FromStr; use serde::{Deserialize, Serialize, Serializer}; +use ts_rs::TS; +use yasi::InternedString; use crate::{Id, InvalidId, SYSTEM_ID}; lazy_static::lazy_static! { pub static ref SYSTEM_PACKAGE_ID: PackageId = PackageId(SYSTEM_ID.clone()); } -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] +#[ts(type = "string")] pub struct PackageId(Id); impl FromStr for PackageId { type Err = InvalidId; @@ -22,10 +25,20 @@ impl From for PackageId { PackageId(id) } } +impl From for Id { + fn from(value: PackageId) -> Self { + value.0 + } +} +impl From for InternedString { + fn from(value: PackageId) -> Self { + value.0.into() + } +} impl std::ops::Deref for PackageId { type Target = str; fn deref(&self) -> &Self::Target { - &*self.0 + &self.0 } } impl AsRef for PackageId { @@ -48,6 +61,11 @@ impl Borrow for PackageId { self.0.as_ref() } } +impl<'a> Borrow for &'a PackageId { + fn borrow(&self) -> &str { + self.0.as_ref() + } +} impl AsRef for PackageId { fn as_ref(&self) -> &Path { self.0.as_ref().as_ref() diff --git a/core/models/src/id/replay.rs b/core/models/src/id/replay.rs new file mode 100644 index 000000000..299b6160a --- /dev/null +++ b/core/models/src/id/replay.rs @@ -0,0 +1,45 @@ +use std::convert::Infallible; +use std::path::Path; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use yasi::InternedString; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(type = "string")] +pub struct ReplayId(InternedString); +impl FromStr for ReplayId { + type Err = Infallible; + fn from_str(s: &str) -> Result { + Ok(ReplayId(InternedString::intern(s))) + } +} +impl AsRef for ReplayId { + fn as_ref(&self) -> &ReplayId { + self + } +} +impl std::fmt::Display for ReplayId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl AsRef for ReplayId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl AsRef for ReplayId { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} +impl<'de> Deserialize<'de> for ReplayId { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + Ok(ReplayId(serde::Deserialize::deserialize(deserializer)?)) + } +} diff --git a/core/models/src/id/address.rs b/core/models/src/id/service_interface.rs similarity index 53% rename from core/models/src/id/address.rs rename to core/models/src/id/service_interface.rs index 1bd670525..f08d89cd5 100644 --- a/core/models/src/id/address.rs +++ b/core/models/src/id/service_interface.rs @@ -1,46 +1,50 @@ use std::path::Path; +use std::str::FromStr; +use rpc_toolkit::clap::builder::ValueParserFactory; use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; -use crate::Id; +use crate::{FromStrParser, Id}; -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -pub struct AddressId(Id); -impl From for AddressId { +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] +#[ts(export, type = "string")] +pub struct ServiceInterfaceId(Id); +impl From for ServiceInterfaceId { fn from(id: Id) -> Self { Self(id) } } -impl std::fmt::Display for AddressId { +impl std::fmt::Display for ServiceInterfaceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0) } } -impl std::ops::Deref for AddressId { +impl std::ops::Deref for ServiceInterfaceId { type Target = str; fn deref(&self) -> &Self::Target { &*self.0 } } -impl AsRef for AddressId { +impl AsRef for ServiceInterfaceId { fn as_ref(&self) -> &str { self.0.as_ref() } } -impl<'de> Deserialize<'de> for AddressId { +impl<'de> Deserialize<'de> for ServiceInterfaceId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - Ok(AddressId(Deserialize::deserialize(deserializer)?)) + Ok(ServiceInterfaceId(Deserialize::deserialize(deserializer)?)) } } -impl AsRef for AddressId { +impl AsRef for ServiceInterfaceId { fn as_ref(&self) -> &Path { self.0.as_ref().as_ref() } } -impl<'q> sqlx::Encode<'q, sqlx::Postgres> for AddressId { +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for ServiceInterfaceId { fn encode_by_ref( &self, buf: &mut >::ArgumentBuffer, @@ -48,7 +52,7 @@ impl<'q> sqlx::Encode<'q, sqlx::Postgres> for AddressId { <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&&**self, buf) } } -impl sqlx::Type for AddressId { +impl sqlx::Type for ServiceInterfaceId { fn type_info() -> sqlx::postgres::PgTypeInfo { <&str as sqlx::Type>::type_info() } @@ -57,3 +61,15 @@ impl sqlx::Type for AddressId { <&str as sqlx::Type>::compatible(ty) } } +impl FromStr for ServiceInterfaceId { + type Err = ::Err; + fn from_str(s: &str) -> Result { + Id::from_str(s).map(Self) + } +} +impl ValueParserFactory for ServiceInterfaceId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} diff --git a/core/models/src/id/volume.rs b/core/models/src/id/volume.rs index 16821a3cf..7425c79c6 100644 --- a/core/models/src/id/volume.rs +++ b/core/models/src/id/volume.rs @@ -2,10 +2,12 @@ use std::borrow::Borrow; use std::path::Path; use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; use crate::Id; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] +#[ts(type = "string")] pub enum VolumeId { Backup, Custom(Id), diff --git a/core/models/src/lib.rs b/core/models/src/lib.rs index ad9055f24..304ac87c5 100644 --- a/core/models/src/lib.rs +++ b/core/models/src/lib.rs @@ -1,3 +1,4 @@ +mod clap; mod data_url; mod errors; mod id; @@ -5,6 +6,7 @@ mod mime; mod procedure_name; mod version; +pub use clap::*; pub use data_url::*; pub use errors::*; pub use id::*; diff --git a/core/models/src/procedure_name.rs b/core/models/src/procedure_name.rs index 6a092955a..4a4a682e6 100644 --- a/core/models/src/procedure_name.rs +++ b/core/models/src/procedure_name.rs @@ -1,57 +1,30 @@ use serde::{Deserialize, Serialize}; -use crate::{ActionId, HealthCheckId, PackageId}; +use crate::ActionId; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProcedureName { - Main, // Usually just run container - CreateBackup, - RestoreBackup, GetConfig, SetConfig, - Migration, - Properties, - LongRunning, - Check(PackageId), - AutoConfig(PackageId), - Health(HealthCheckId), - Action(ActionId), - Signal, + CreateBackup, + RestoreBackup, + GetActionInput(ActionId), + RunAction(ActionId), + PackageInit, + PackageUninit, } impl ProcedureName { - pub fn docker_name(&self) -> Option { - match self { - ProcedureName::Main => None, - ProcedureName::LongRunning => None, - ProcedureName::CreateBackup => Some("CreateBackup".to_string()), - ProcedureName::RestoreBackup => Some("RestoreBackup".to_string()), - ProcedureName::GetConfig => Some("GetConfig".to_string()), - ProcedureName::SetConfig => Some("SetConfig".to_string()), - ProcedureName::Migration => Some("Migration".to_string()), - ProcedureName::Properties => Some(format!("Properties-{}", rand::random::())), - ProcedureName::Health(id) => Some(format!("{}Health", id)), - ProcedureName::Action(id) => Some(format!("{}Action", id)), - ProcedureName::Check(_) => None, - ProcedureName::AutoConfig(_) => None, - ProcedureName::Signal => None, - } - } - pub fn js_function_name(&self) -> Option { + pub fn js_function_name(&self) -> String { match self { - ProcedureName::Main => Some("/main".to_string()), - ProcedureName::LongRunning => None, - ProcedureName::CreateBackup => Some("/createBackup".to_string()), - ProcedureName::RestoreBackup => Some("/restoreBackup".to_string()), - ProcedureName::GetConfig => Some("/getConfig".to_string()), - ProcedureName::SetConfig => Some("/setConfig".to_string()), - ProcedureName::Migration => Some("/migration".to_string()), - ProcedureName::Properties => Some("/properties".to_string()), - ProcedureName::Health(id) => Some(format!("/health/{}", id)), - ProcedureName::Action(id) => Some(format!("/action/{}", id)), - ProcedureName::Check(id) => Some(format!("/dependencies/{}/check", id)), - ProcedureName::AutoConfig(id) => Some(format!("/dependencies/{}/autoConfigure", id)), - ProcedureName::Signal => Some("/handleSignal".to_string()), + ProcedureName::PackageInit => "/packageInit".to_string(), + ProcedureName::PackageUninit => "/packageUninit".to_string(), + ProcedureName::SetConfig => "/config/set".to_string(), + ProcedureName::GetConfig => "/config/get".to_string(), + ProcedureName::CreateBackup => "/backup/create".to_string(), + ProcedureName::RestoreBackup => "/backup/restore".to_string(), + ProcedureName::RunAction(id) => format!("/actions/{}/run", id), + ProcedureName::GetActionInput(id) => format!("/actions/{}/getInput", id), } } } diff --git a/core/models/src/version.rs b/core/models/src/version.rs index 1e4798ba1..f0c7b19ae 100644 --- a/core/models/src/version.rs +++ b/core/models/src/version.rs @@ -3,100 +3,109 @@ use std::ops::Deref; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use ts_rs::TS; -#[derive(Debug, Clone)] -pub struct Version { - version: emver::Version, +#[derive(Debug, Clone, TS)] +#[ts(type = "string", rename = "Version")] +pub struct VersionString { + version: exver::ExtendedVersion, string: String, } -impl Version { +impl VersionString { pub fn as_str(&self) -> &str { self.string.as_str() } - pub fn into_version(self) -> emver::Version { + pub fn into_version(self) -> exver::ExtendedVersion { self.version } } -impl std::fmt::Display for Version { +impl std::fmt::Display for VersionString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.string) } } -impl std::str::FromStr for Version { - type Err = ::Err; +impl std::str::FromStr for VersionString { + type Err = ::Err; fn from_str(s: &str) -> Result { - Ok(Version { + Ok(VersionString { string: s.to_owned(), version: s.parse()?, }) } } -impl From for Version { - fn from(v: emver::Version) -> Self { - Version { +impl From for VersionString { + fn from(v: exver::ExtendedVersion) -> Self { + VersionString { string: v.to_string(), version: v, } } } -impl From for emver::Version { - fn from(v: Version) -> Self { +impl From for exver::ExtendedVersion { + fn from(v: VersionString) -> Self { v.version } } -impl Default for Version { +impl Default for VersionString { fn default() -> Self { - Self::from(emver::Version::default()) + Self::from(exver::ExtendedVersion::default()) } } -impl Deref for Version { - type Target = emver::Version; +impl Deref for VersionString { + type Target = exver::ExtendedVersion; fn deref(&self) -> &Self::Target { &self.version } } -impl AsRef for Version { - fn as_ref(&self) -> &emver::Version { +impl AsRef for VersionString { + fn as_ref(&self) -> &exver::ExtendedVersion { &self.version } } -impl AsRef for Version { +impl AsRef for VersionString { fn as_ref(&self) -> &str { self.as_str() } } -impl PartialEq for Version { - fn eq(&self, other: &Version) -> bool { +impl PartialEq for VersionString { + fn eq(&self, other: &VersionString) -> bool { self.version.eq(&other.version) } } -impl Eq for Version {} -impl PartialOrd for Version { +impl Eq for VersionString {} +impl PartialOrd for VersionString { fn partial_cmp(&self, other: &Self) -> Option { self.version.partial_cmp(&other.version) } } -impl Ord for Version { +impl Ord for VersionString { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) + self.version.partial_cmp(&other.version).unwrap_or_else(|| { + match (self.version.flavor(), other.version.flavor()) { + (None, Some(_)) => std::cmp::Ordering::Greater, + (Some(_), None) => std::cmp::Ordering::Less, + (a, b) => a.cmp(&b), + } + }) } } -impl Hash for Version { +impl Hash for VersionString { fn hash(&self, state: &mut H) { self.version.hash(state) } } -impl<'de> Deserialize<'de> for Version { +impl<'de> Deserialize<'de> for VersionString { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let string = String::deserialize(deserializer)?; - let version = emver::Version::from_str(&string).map_err(::serde::de::Error::custom)?; + let version = + exver::ExtendedVersion::from_str(&string).map_err(::serde::de::Error::custom)?; Ok(Self { string, version }) } } -impl Serialize for Version { +impl Serialize for VersionString { fn serialize(&self, serializer: S) -> Result where S: Serializer, diff --git a/core/run-tests.sh b/core/run-tests.sh new file mode 100755 index 000000000..02ec34d55 --- /dev/null +++ b/core/run-tests.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "apt-get update && apt-get install -y rsync && cd core && cargo test --release --features=test,$FEATURES --workspace --locked --target=$ARCH-unknown-linux-musl -- --skip export_bindings_ && chown \$UID:\$UID target" +if [ "$(ls -nd core/target | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi \ No newline at end of file diff --git a/core/snapshot-creator/Cargo.toml b/core/snapshot-creator/Cargo.toml deleted file mode 100644 index 628cd3161..000000000 --- a/core/snapshot-creator/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "snapshot_creator" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -dashmap = "5.3.4" -deno_core = "=0.222.0" -deno_ast = { version = "=0.29.5", features = ["transpiling"] } diff --git a/core/snapshot-creator/src/main.rs b/core/snapshot-creator/src/main.rs deleted file mode 100644 index ad7330484..000000000 --- a/core/snapshot-creator/src/main.rs +++ /dev/null @@ -1,11 +0,0 @@ -use deno_core::JsRuntimeForSnapshot; - -fn main() { - let runtime = JsRuntimeForSnapshot::new(Default::default()); - let snapshot = runtime.snapshot(); - - let snapshot_slice: &[u8] = &*snapshot; - println!("Snapshot size: {}", snapshot_slice.len()); - - std::fs::write("JS_SNAPSHOT.bin", snapshot_slice).unwrap(); -} diff --git a/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json b/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json deleted file mode 100644 index d36100fef..000000000 --- a/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [] - }, - "hash": "1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e" -} diff --git a/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json b/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json deleted file mode 100644 index e0b1d7cf2..000000000 --- a/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM ssh_keys WHERE fingerprint = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930" -} diff --git a/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json b/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json deleted file mode 100644 index e234a72a9..000000000 --- a/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false, - false, - true - ] - }, - "hash": "28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec" -} diff --git a/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json b/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json deleted file mode 100644 index c451ce9f3..000000000 --- a/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM tor WHERE package = $1 AND interface = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962" -} diff --git a/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json b/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json deleted file mode 100644 index 761af064b..000000000 --- a/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM ssh_keys WHERE fingerprint = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "fingerprint", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "openssh_pubkey", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997" -} diff --git a/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json b/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json deleted file mode 100644 index 1f7edd1ce..000000000 --- a/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "logged_in", - "type_info": "Timestamp" - }, - { - "ordinal": 2, - "name": "logged_out", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "last_active", - "type_info": "Timestamp" - }, - { - "ordinal": 4, - "name": "user_agent", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "metadata", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - true, - false, - true, - false - ] - }, - "hash": "4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96" -} diff --git a/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json b/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json deleted file mode 100644 index 2157198e5..000000000 --- a/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a" -} diff --git a/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json b/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json deleted file mode 100644 index 764cff84a..000000000 --- a/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT password FROM account", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a" -} diff --git a/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json b/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json deleted file mode 100644 index 2e8a9ee0e..000000000 --- a/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT key FROM tor WHERE package = $1 AND interface = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d" -} diff --git a/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json b/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json deleted file mode 100644 index 3f859bd10..000000000 --- a/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = $1 AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca" -} diff --git a/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json b/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json deleted file mode 100644 index cf3591e01..000000000 --- a/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO UPDATE SET package = EXCLUDED.package RETURNING key", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [ - false - ] - }, - "hash": "770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0" -} diff --git a/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json b/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json deleted file mode 100644 index 53fc6f066..000000000 --- a/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications WHERE id < $1 ORDER BY id DESC LIMIT $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "package_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "code", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "level", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "title", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "message", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "data", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [ - false, - true, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210" -} diff --git a/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json b/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json deleted file mode 100644 index 245a838d8..000000000 --- a/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO account (\n id,\n server_id,\n hostname,\n password,\n network_key,\n root_ca_key_pem,\n root_ca_cert_pem\n ) VALUES (\n 0, $1, $2, $3, $4, $5, $6\n ) ON CONFLICT (id) DO UPDATE SET\n server_id = EXCLUDED.server_id,\n hostname = EXCLUDED.hostname,\n password = EXCLUDED.password,\n network_key = EXCLUDED.network_key,\n root_ca_key_pem = EXCLUDED.root_ca_key_pem,\n root_ca_cert_pem = EXCLUDED.root_ca_cert_pem\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Bytea", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb" -} diff --git a/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json b/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json deleted file mode 100644 index e3ce7957d..000000000 --- a/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM tor WHERE package = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576" -} diff --git a/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json b/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json deleted file mode 100644 index e39aebf69..000000000 --- a/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO tor (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [] - }, - "hash": "8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5" -} diff --git a/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json b/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json deleted file mode 100644 index e7fe8d38c..000000000 --- a/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications ORDER BY id DESC LIMIT $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "package_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "code", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "level", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "title", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "message", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "data", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - true, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c" -} diff --git a/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json b/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json deleted file mode 100644 index aadc0fc3a..000000000 --- a/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, hostname, path, username, password FROM cifs_shares", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "path", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true - ] - }, - "hash": "95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a" -} diff --git a/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json b/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json deleted file mode 100644 index c56a9ebd1..000000000 --- a/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM cifs_shares WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27" -} diff --git a/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json b/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json deleted file mode 100644 index 86bd9250e..000000000 --- a/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT fingerprint, openssh_pubkey, created_at FROM ssh_keys", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "fingerprint", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "openssh_pubkey", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e" -} diff --git a/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json b/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json deleted file mode 100644 index c8ff84277..000000000 --- a/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE cifs_shares SET hostname = $1, path = $2, username = $3, password = $4 WHERE id = $5", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6" -} diff --git a/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json b/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json deleted file mode 100644 index b76542db8..000000000 --- a/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM network_keys WHERE package = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d" -} diff --git a/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json b/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json deleted file mode 100644 index e2e8a1620..000000000 --- a/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE account SET tor_key = NULL, network_key = gen_random_bytes(32)", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1" -} diff --git a/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json b/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json new file mode 100644 index 000000000..d5fae12b7 --- /dev/null +++ b/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae" +} diff --git a/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json b/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json deleted file mode 100644 index b77ba7ce9..000000000 --- a/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT openssh_pubkey FROM ssh_keys", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "openssh_pubkey", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca" -} diff --git a/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json b/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json deleted file mode 100644 index 5c5c89c27..000000000 --- a/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO notifications (package_id, code, level, title, message, data) VALUES ($1, $2, $3, $4, $5, $6)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int4", - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b" -} diff --git a/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json b/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json deleted file mode 100644 index 2fc8ad1ba..000000000 --- a/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM network_keys WHERE package = $1 AND interface = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524" -} diff --git a/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json b/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json deleted file mode 100644 index a4dc187cd..000000000 --- a/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM notifications WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7" -} diff --git a/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json b/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json deleted file mode 100644 index 97a4ec95a..000000000 --- a/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT tor_key FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "tor_key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - true - ] - }, - "hash": "e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0" -} diff --git a/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json b/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json deleted file mode 100644 index b2aa04370..000000000 --- a/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO session (id, user_agent, metadata) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d" -} diff --git a/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json b/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json deleted file mode 100644 index fd5a467ec..000000000 --- a/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n network_keys.package,\n network_keys.interface,\n network_keys.key,\n tor.key AS \"tor_key?\"\n FROM\n network_keys\n LEFT JOIN\n tor\n ON\n network_keys.package = tor.package\n AND\n network_keys.interface = tor.interface\n WHERE\n network_keys.package = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "package", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "interface", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "key", - "type_info": "Bytea" - }, - { - "ordinal": 3, - "name": "tor_key?", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed" -} diff --git a/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json b/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json deleted file mode 100644 index fb8a7c1e5..000000000 --- a/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM notifications WHERE id < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a" -} diff --git a/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json b/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json deleted file mode 100644 index 27c9752b2..000000000 --- a/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO cifs_shares (hostname, path, username, password) VALUES ($1, $2, $3, $4) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded" -} diff --git a/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json b/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json deleted file mode 100644 index 6ed9898f6..000000000 --- a/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO ssh_keys (fingerprint, openssh_pubkey, created_at) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556" -} diff --git a/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json b/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json deleted file mode 100644 index f48ccb074..000000000 --- a/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT network_key FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5" -} diff --git a/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json b/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json deleted file mode 100644 index 6ef1d5023..000000000 --- a/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "password", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "tor_key", - "type_info": "Bytea" - }, - { - "ordinal": 3, - "name": "server_id", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "network_key", - "type_info": "Bytea" - }, - { - "ordinal": 6, - "name": "root_ca_key_pem", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "root_ca_cert_pem", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - false - ] - }, - "hash": "fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f" -} diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 65d01b1db..df1622851 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.5-rev.1" +version = "0.3.6-alpha.13" # VERSION_BUMP license = "MIT" [lib] @@ -25,19 +25,35 @@ path = "src/lib.rs" name = "startbox" path = "src/main.rs" +[[bin]] +name = "start-cli" +path = "src/main.rs" + +[[bin]] +name = "containerbox" +path = "src/main.rs" + +[[bin]] +name = "registrybox" +path = "src/main.rs" + [features] -avahi = ["avahi-sys"] -avahi-alias = ["avahi"] cli = [] +container-runtime = ["procfs", "tty-spawn"] daemon = [] -default = ["cli", "sdk", "daemon", "js-engine"] +registry = [] +default = ["cli", "daemon", "registry", "container-runtime"] dev = [] -docker = [] -sdk = [] unstable = ["console-subscriber", "tokio/tracing"] +docker = [] +test = [] [dependencies] aes = { version = "0.7.5", features = ["ctr"] } +async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [ + "use_rustls", + "use_tokio", +] } async-compression = { version = "0.4.4", features = [ "gzip", "brotli", @@ -45,69 +61,98 @@ async-compression = { version = "0.4.4", features = [ ] } async-stream = "0.3.5" async-trait = "0.1.74" -avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", version = "0.10.0", branch = "feature/dynamic-linking", features = [ - "dynamic", -], optional = true } -base32 = "0.4.0" -base64 = "0.21.4" +axum = { version = "0.7.3", features = ["ws"] } +barrage = "0.2.3" +backhand = "0.18.0" +base32 = "0.5.0" +base64 = "0.22.1" base64ct = "1.6.0" basic-cookies = "0.1.4" +blake3 = { version = "1.5.0", features = ["mmap", "rayon"] } bytes = "1" chrono = { version = "0.4.31", features = ["serde"] } -clap = "3.2.25" +clap = "4.4.12" color-eyre = "0.6.2" console = "0.15.7" -console-subscriber = { version = "0.2", optional = true } +console-subscriber = { version = "0.3.0", optional = true } +const_format = "0.2.34" cookie = "0.18.0" -cookie_store = "0.20.0" -current_platform = "0.2.0" +cookie_store = "0.21.0" +der = { version = "0.7.9", features = ["derive", "pem"] } digest = "0.10.7" divrem = "1.0.0" ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] } -ed25519-dalek = { version = "2.0.0", features = [ +ed25519-dalek = { version = "2.1.1", features = [ "serde", "zeroize", "rand_core", "digest", + "pkcs8", ] } ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" } -container-init = { path = "../container-init" } -emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [ +exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ "serde", ] } fd-lock-rs = "0.1.4" +form_urlencoded = "1.2.1" futures = "0.3.28" gpt = "3.1.0" helpers = { path = "../helpers" } hex = "0.4.3" hmac = "0.12.1" -http = "0.2.9" -hyper = { version = "0.14.27", features = ["full"] } -hyper-ws-listener = "0.3.0" -imbl = "2.0.2" -imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" } -include_dir = "0.7.3" +http = "1.0.0" +http-body-util = "0.1" +hyper = { version = "1.5", features = ["server", "http1", "http2"] } +hyper-util = { version = "0.1.10", features = [ + "server", + "server-auto", + "server-graceful", + "service", + "http1", + "http2", + "tokio", +] } +id-pool = { version = "0.2.2", default-features = false, features = [ + "serde", + "u16", +] } +imbl = "2.0.3" +imbl-value = "0.1.2" +include_dir = { version = "0.7.3", features = ["metadata"] } indexmap = { version = "2.0.2", features = ["serde"] } indicatif = { version = "0.17.7", features = ["tokio"] } +integer-encoding = { version = "4.0.0", features = ["tokio_async"] } ipnet = { version = "2.8.0", features = ["serde"] } iprange = { version = "0.6.7", features = ["serde"] } isocountry = "0.3.2" -itertools = "0.11.0" +itertools = "0.13.0" jaq-core = "0.10.1" jaq-std = "0.10.0" josekit = "0.8.4" -js-engine = { path = '../js-engine', optional = true } jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" } +lazy_async_pool = "0.3.3" +lazy_format = "2.0" lazy_static = "1.4.0" libc = "0.2.149" log = "0.4.20" +mio = "1" mbrman = "0.5.2" models = { version = "*", path = "../models" } new_mime_guess = "4" -nix = { version = "0.27.1", features = ["user", "process", "signal", "fs"] } +nix = { version = "0.29.0", features = [ + "fs", + "mount", + "net", + "process", + "sched", + "signal", + "user", +] } nom = "7.1.3" num = "0.4.1" num_enum = "0.7.0" +num_cpus = "1.16.0" +once_cell = "1.19.0" openssh-keys = "0.6.2" openssl = { version = "0.10.57", features = ["vendored"] } p256 = { version = "0.13.2", features = ["pem"] } @@ -118,25 +163,31 @@ pbkdf2 = "0.12.2" pin-project = "1.1.3" pkcs8 = { version = "0.10.2", features = ["std"] } prettytable-rs = "0.10.0" +procfs = { version = "0.16.0", optional = true } proptest = "1.3.1" -proptest-derive = "0.4.0" +proptest-derive = "0.5.0" +qrcode = "0.14.1" rand = { version = "0.8.5", features = ["std"] } regex = "1.10.2" -reqwest = { version = "0.11.22", features = ["stream", "json", "socks"] } -reqwest_cookie_store = "0.6.0" +reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] } +reqwest_cookie_store = "0.8.0" rpassword = "7.2.0" -rpc-toolkit = "0.2.2" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" } rust-argon2 = "2.0.0" -scopeguard = "1.1" # because avahi-sys fucks your shit up +rustyline-async = "0.4.1" semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_cbor = { package = "ciborium", version = "0.2.1" } serde_json = "1.0" serde_toml = { package = "toml", version = "0.8.2" } +serde_urlencoded = "0.7" serde_with = { version = "3.4.0", features = ["macros", "json"] } -serde_yaml = "0.9.25" +serde_yaml = { package = "serde_yml", version = "0.0.10" } sha2 = "0.10.2" +shell-words = "1" +signal-hook = "0.3.17" simple-logging = "2.0.2" +socket2 = "0.5.7" sqlx = { version = "0.7.2", features = [ "chrono", "runtime-tokio-rustls", @@ -144,28 +195,38 @@ sqlx = { version = "0.7.2", features = [ ] } sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } -stderrlog = "0.5.4" tar = "0.4.40" thiserror = "1.0.49" -tokio = { version = "1", features = ["full"] } -tokio-rustls = "0.24.1" +textwrap = "0.16.1" +tokio = { version = "1.38.1", features = ["full"] } +tokio-rustls = "0.26.0" tokio-socks = "0.5.1" tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } -tokio-tungstenite = { version = "0.20.1", features = ["native-tls"] } +tokio-tungstenite = { version = "0.23.1", features = ["native-tls", "url"] } tokio-util = { version = "0.7.9", features = ["io"] } -torut = "0.2.1" +torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies", features = [ + "serialize", +] } +tower-service = "0.3.3" tracing = "0.1.39" tracing-error = "0.2.0" tracing-futures = "0.2.5" tracing-journald = "0.3.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } trust-dns-server = "0.23.1" -typed-builder = "0.17.0" +ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0" +tty-spawn = { version = "0.4.0", optional = true } +typed-builder = "0.18.0" +unix-named-pipe = "0.2.0" url = { version = "2.4.1", features = ["serde"] } urlencoding = "2.1.3" uuid = { version = "1.4.1", features = ["v4"] } +zbus = "5.1.1" zeroize = "1.6.0" +mail-send = { git = "https://github.com/dr-bonez/mail-send.git", branch = "main" } +rustls = "0.23.20" +rustls-pki-types = { version = "1.10.1", features = ["alloc"] } [profile.test] opt-level = 3 diff --git a/core/startos/Effects.ts b/core/startos/Effects.ts new file mode 100644 index 000000000..9be56724e --- /dev/null +++ b/core/startos/Effects.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface SetStoreParams { value: any, path: string, } \ No newline at end of file diff --git a/core/startos/deny.toml b/core/startos/deny.toml index 7b4924cdc..5a42f7378 100644 --- a/core/startos/deny.toml +++ b/core/startos/deny.toml @@ -14,9 +14,15 @@ allow = [ "BSD-3-Clause", "LGPL-3.0", "OpenSSL", + "Unicode-DFS-2016", + "Zlib", ] clarify = [ - { name = "webpki", expression = "ISC", license-files = [ { path = "LICENSE", hash = 0x001c7e6c } ] }, - { name = "ring", expression = "OpenSSL", license-files = [ { path = "LICENSE", hash = 0xbd0eed23 } ] }, + { name = "webpki", expression = "ISC", license-files = [ + { path = "LICENSE", hash = 0x001c7e6c }, + ] }, + { name = "ring", expression = "OpenSSL", license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, + ] }, ] diff --git a/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt b/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt new file mode 100644 index 000000000..116de6aba --- /dev/null +++ b/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc dbb4790c31f9e400ed29a9ba2dbd61e3c55ce8a3fbae16601ca3512e803020ed # shrinks to files = [] diff --git a/core/startos/registry.service b/core/startos/registry.service new file mode 100644 index 000000000..63941a25e --- /dev/null +++ b/core/startos/registry.service @@ -0,0 +1,13 @@ +[Unit] +Description=StartOS Registry + +[Service] +Type=simple +Environment=RUST_LOG=startos=debug,patch_db=warn +ExecStart=/usr/local/bin/registry +Restart=always +RestartSec=3 +ManagedOOMPreference=avoid + +[Install] +WantedBy=multi-user.target diff --git a/core/startos/src/account.rs b/core/startos/src/account.rs index cb08a0d53..389589667 100644 --- a/core/startos/src/account.rs +++ b/core/startos/src/account.rs @@ -1,15 +1,14 @@ use std::time::SystemTime; -use ed25519_dalek::SecretKey; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; -use sqlx::PgExecutor; +use torut::onion::TorSecretKeyV3; +use crate::db::model::DatabaseModel; use crate::hostname::{generate_hostname, generate_id, Hostname}; -use crate::net::keys::Key; use crate::net::ssl::{generate_key, make_root_cert}; use crate::prelude::*; -use crate::util::crypto::ed25519_expand_key; +use crate::util::serde::Pem; fn hash_password(password: &str) -> Result { argon2::hash_encoded( @@ -25,103 +24,95 @@ pub struct AccountInfo { pub server_id: String, pub hostname: Hostname, pub password: String, - pub key: Key, + pub tor_keys: Vec, pub root_ca_key: PKey, pub root_ca_cert: X509, + pub ssh_key: ssh_key::PrivateKey, + pub compat_s9pk_key: ed25519_dalek::SigningKey, } impl AccountInfo { pub fn new(password: &str, start_time: SystemTime) -> Result { let server_id = generate_id(); let hostname = generate_hostname(); + let tor_key = vec![TorSecretKeyV3::generate()]; let root_ca_key = generate_key()?; let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; + let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( + &mut rand::thread_rng(), + )); + let compat_s9pk_key = ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()); Ok(Self { server_id, hostname, password: hash_password(password)?, - key: Key::new(None), + tor_keys: tor_key, root_ca_key, root_ca_cert, + ssh_key, + compat_s9pk_key, }) } - pub async fn load(secrets: impl PgExecutor<'_>) -> Result { - let r = sqlx::query!("SELECT * FROM account WHERE id = 0") - .fetch_one(secrets) - .await?; - - let server_id = r.server_id.unwrap_or_else(generate_id); - let hostname = r.hostname.map(Hostname).unwrap_or_else(generate_hostname); - let password = r.password; - let network_key = SecretKey::try_from(r.network_key).map_err(|e| { - Error::new( - eyre!("expected vec of len 32, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?; - let tor_key = if let Some(k) = &r.tor_key { - <[u8; 64]>::try_from(&k[..]).map_err(|_| { - Error::new( - eyre!("expected vec of len 64, got len {}", k.len()), - ErrorKind::ParseDbField, - ) - })? - } else { - ed25519_expand_key(&network_key) - }; - let key = Key::from_pair(None, network_key, tor_key); - let root_ca_key = PKey::private_key_from_pem(r.root_ca_key_pem.as_bytes())?; - let root_ca_cert = X509::from_pem(r.root_ca_cert_pem.as_bytes())?; + pub fn load(db: &DatabaseModel) -> Result { + let server_id = db.as_public().as_server_info().as_id().de()?; + let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); + let password = db.as_private().as_password().de()?; + let key_store = db.as_private().as_key_store(); + let tor_addrs = db.as_public().as_server_info().as_host().as_onions().de()?; + let tor_keys = tor_addrs + .into_iter() + .map(|tor_addr| key_store.as_onion().get_key(&tor_addr)) + .collect::>()?; + let cert_store = key_store.as_local_certs(); + let root_ca_key = cert_store.as_root_key().de()?.0; + let root_ca_cert = cert_store.as_root_cert().de()?.0; + let ssh_key = db.as_private().as_ssh_privkey().de()?.0; + let compat_s9pk_key = db.as_private().as_compat_s9pk_key().de()?.0; Ok(Self { server_id, hostname, password, - key, + tor_keys, root_ca_key, root_ca_cert, + ssh_key, + compat_s9pk_key, }) } - pub async fn save(&self, secrets: impl PgExecutor<'_>) -> Result<(), Error> { - let server_id = self.server_id.as_str(); - let hostname = self.hostname.0.as_str(); - let password = self.password.as_str(); - let network_key = self.key.as_bytes(); - let network_key = network_key.as_slice(); - let root_ca_key = String::from_utf8(self.root_ca_key.private_key_to_pem_pkcs8()?)?; - let root_ca_cert = String::from_utf8(self.root_ca_cert.to_pem()?)?; - - sqlx::query!( - r#" - INSERT INTO account ( - id, - server_id, - hostname, - password, - network_key, - root_ca_key_pem, - root_ca_cert_pem - ) VALUES ( - 0, $1, $2, $3, $4, $5, $6 - ) ON CONFLICT (id) DO UPDATE SET - server_id = EXCLUDED.server_id, - hostname = EXCLUDED.hostname, - password = EXCLUDED.password, - network_key = EXCLUDED.network_key, - root_ca_key_pem = EXCLUDED.root_ca_key_pem, - root_ca_cert_pem = EXCLUDED.root_ca_cert_pem - "#, - server_id, - hostname, - password, - network_key, - root_ca_key, - root_ca_cert, - ) - .execute(secrets) - .await?; - + pub fn save(&self, db: &mut DatabaseModel) -> Result<(), Error> { + let server_info = db.as_public_mut().as_server_info_mut(); + server_info.as_id_mut().ser(&self.server_id)?; + server_info.as_hostname_mut().ser(&self.hostname.0)?; + server_info + .as_pubkey_mut() + .ser(&self.ssh_key.public_key().to_openssh()?)?; + server_info.as_host_mut().as_onions_mut().ser( + &self + .tor_keys + .iter() + .map(|tor_key| tor_key.public().get_onion_address()) + .collect(), + )?; + db.as_private_mut().as_password_mut().ser(&self.password)?; + db.as_private_mut() + .as_ssh_privkey_mut() + .ser(Pem::new_ref(&self.ssh_key))?; + db.as_private_mut() + .as_compat_s9pk_key_mut() + .ser(Pem::new_ref(&self.compat_s9pk_key))?; + let key_store = db.as_private_mut().as_key_store_mut(); + for tor_key in &self.tor_keys { + key_store.as_onion_mut().insert_key(tor_key)?; + } + let cert_store = key_store.as_local_certs_mut(); + cert_store + .as_root_key_mut() + .ser(Pem::new_ref(&self.root_ca_key))?; + cert_store + .as_root_cert_mut() + .ser(Pem::new_ref(&self.root_ca_cert))?; Ok(()) } diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index 3223aaa86..b29768d73 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -1,163 +1,311 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; -use clap::ArgMatches; -use color_eyre::eyre::eyre; -use indexmap::IndexSet; +use clap::{CommandFactory, FromArgMatches, Parser}; pub use models::ActionId; -use models::ImageId; -use rpc_toolkit::command; +use models::PackageId; +use qrcode::QrCode; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; +use ts_rs::TS; -use crate::config::{Config, ConfigSpec}; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; use crate::prelude::*; -use crate::procedure::docker::DockerContainers; -use crate::procedure::{PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat}; -use crate::util::Version; -use crate::volume::Volumes; -use crate::{Error, ResultExt}; -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct Actions(pub BTreeMap); +use crate::rpc_continuations::Guid; +use crate::util::serde::{ + display_serializable, HandlerExtSerde, StdinDeserializable, WithIoFormat, +}; -#[derive(Debug, Serialize, Deserialize)] +pub fn action_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "get-input", + from_fn_async(get_action_input) + .with_display_serializable() + .with_about("Get action input spec") + .with_call_remote::(), + ) + .subcommand( + "run", + from_fn_async(run_action) + .with_display_serializable() + .with_custom_display_fn(|_, res| { + if let Some(res) = res { + println!("{res}") + } + Ok(()) + }) + .with_about("Run service action") + .with_call_remote::(), + ) +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ActionInput { + #[ts(type = "Record")] + pub spec: Value, + #[ts(type = "Record | null")] + pub value: Option, +} + +#[derive(Deserialize, Serialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +pub struct GetActionInputParams { + pub package_id: PackageId, + pub action_id: ActionId, +} + +#[instrument(skip_all)] +pub async fn get_action_input( + ctx: RpcContext, + GetActionInputParams { + package_id, + action_id, + }: GetActionInputParams, +) -> Result, Error> { + ctx.services + .get(&package_id) + .await + .as_ref() + .or_not_found(lazy_format!("Manager for {}", package_id))? + .get_action_input(Guid::new(), action_id) + .await +} + +#[derive(Debug, Serialize, Deserialize, TS)] #[serde(tag = "version")] +#[ts(export)] pub enum ActionResult { #[serde(rename = "0")] V0(ActionResultV0), + #[serde(rename = "1")] + V1(ActionResultV1), +} +impl fmt::Display for ActionResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V0(res) => res.fmt(f), + Self::V1(res) => res.fmt(f), + } + } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TS)] pub struct ActionResultV0 { pub message: String, pub value: Option, pub copyable: bool, pub qr: bool, } +impl fmt::Display for ActionResultV0 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message)?; + if let Some(value) = &self.value { + write!(f, ":\n{value}")?; + if self.qr { + use qrcode::render::unicode; + write!( + f, + "\n{}", + QrCode::new(value.as_bytes()) + .unwrap() + .render::() + .build() + )?; + } + } + Ok(()) + } +} -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum DockerStatus { - Running, - Stopped, +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ActionResultV1 { + /// Primary text to display as the header of the response modal. e.g. "Success!", "Name Updated", or "Service Information", whatever makes sense + pub title: String, + /// (optional) A general message for the user, just under the title + pub message: Option, + /// (optional) Structured data to present inside the modal + pub result: Option, } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Action { +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ActionResultMember { + /// A human-readable name or title of the value, such as "Last Active" or "Login Password" pub name: String, - pub description: String, - #[serde(default)] - pub warning: Option, - pub implementation: PackageProcedure, - pub allowed_statuses: IndexSet, - #[serde(default)] - pub input_spec: ConfigSpec, -} -impl Action { - #[instrument(skip_all)] - pub fn validate( - &self, - _container: &Option, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - ) -> Result<(), Error> { - self.implementation - .validate(eos_version, volumes, image_ids, true) - .with_ctx(|_| { - ( - crate::ErrorKind::ValidateS9pk, - format!("Action {}", self.name), - ) - }) - } + /// (optional) A description of the value, such as an explaining why it exists or how to use it + pub description: Option, + #[serde(flatten)] + #[ts(flatten)] + pub value: ActionResultValue, +} - #[instrument(skip_all)] - pub async fn execute( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - action_id: &ActionId, - volumes: &Volumes, - input: Option, - ) -> Result { - if let Some(ref input) = input { - self.input_spec - .matches(&input) - .with_kind(crate::ErrorKind::ConfigSpecViolation)?; +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] +#[serde(tag = "type")] +pub enum ActionResultValue { + Single { + /// The actual string value to display + value: String, + /// Whether or not to include a copy to clipboard icon to copy the value + copyable: bool, + /// Whether or not to also display the value as a QR code + qr: bool, + /// Whether or not to mask the value using ●●●●●●●, which is useful for password or other sensitive information + masked: bool, + }, + Group { + /// An new group of nested values, experienced by the user as an accordion dropdown + value: Vec, + }, +} +impl ActionResultValue { + fn fmt_rec(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result { + match self { + Self::Single { value, qr, .. } => { + for _ in 0..indent { + write!(f, " ")?; + } + write!(f, "{value}")?; + if *qr { + use qrcode::render::unicode; + writeln!(f)?; + for _ in 0..indent { + write!(f, " ")?; + } + write!( + f, + "{}", + QrCode::new(value.as_bytes()) + .unwrap() + .render::() + .build() + )?; + } + } + Self::Group { value } => { + for ActionResultMember { + name, + description, + value, + } in value + { + for _ in 0..indent { + write!(f, " ")?; + } + write!(f, "{name}")?; + if let Some(description) = description { + write!(f, ": {description}")?; + } + writeln!(f, ":")?; + value.fmt_rec(f, indent + 1)?; + } + } } - self.implementation - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::Action(action_id.clone()), - volumes, - input, - None, - ) - .await? - .map_err(|e| Error::new(eyre!("{}", e.1), crate::ErrorKind::Action)) + Ok(()) + } +} +impl fmt::Display for ActionResultV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{}:", self.title)?; + if let Some(message) = &self.message { + writeln!(f, "{message}")?; + } + if let Some(result) = &self.result { + result.fmt_rec(f, 1)?; + } + Ok(()) } } -fn display_action_result(action_result: ActionResult, matches: &ArgMatches) { - if matches.is_present("format") { - return display_serializable(action_result, matches); +pub fn display_action_result(params: WithIoFormat, result: Option) { + let Some(result) = result else { + return; + }; + if let Some(format) = params.format { + return display_serializable(format, result); } - match action_result { - ActionResult::V0(ar) => { - println!( - "{}: {}", - ar.message, - serde_json::to_string(&ar.value).unwrap() - ); + println!("{result}") +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct RunActionParams { + pub package_id: PackageId, + pub action_id: ActionId, + #[ts(optional, type = "any")] + pub input: Option, +} + +#[derive(Parser)] +struct CliRunActionParams { + pub package_id: PackageId, + pub action_id: ActionId, + #[command(flatten)] + pub input: StdinDeserializable>, +} +impl From for RunActionParams { + fn from( + CliRunActionParams { + package_id, + action_id, + input, + }: CliRunActionParams, + ) -> Self { + Self { + package_id, + action_id, + input: input.0, } } } +impl CommandFactory for RunActionParams { + fn command() -> clap::Command { + CliRunActionParams::command() + } + fn command_for_update() -> clap::Command { + CliRunActionParams::command_for_update() + } +} +impl FromArgMatches for RunActionParams { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + CliRunActionParams::from_arg_matches(matches).map(Self::from) + } + fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { + CliRunActionParams::from_arg_matches_mut(matches).map(Self::from) + } + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + *self = CliRunActionParams::from_arg_matches(matches).map(Self::from)?; + Ok(()) + } + fn update_from_arg_matches_mut( + &mut self, + matches: &mut clap::ArgMatches, + ) -> Result<(), clap::Error> { + *self = CliRunActionParams::from_arg_matches_mut(matches).map(Self::from)?; + Ok(()) + } +} -#[command(about = "Executes an action", display(display_action_result))] +// #[command(about = "Executes an action", display(display_action_result))] #[instrument(skip_all)] -pub async fn action( - #[context] ctx: RpcContext, - #[arg(rename = "id")] pkg_id: PackageId, - #[arg(rename = "action-id")] action_id: ActionId, - #[arg(stdin, parse(parse_stdin_deserializable))] input: Option, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { - let manifest = ctx - .db - .peek() +pub async fn run_action( + ctx: RpcContext, + RunActionParams { + package_id, + action_id, + input, + }: RunActionParams, +) -> Result, Error> { + ctx.services + .get(&package_id) + .await + .as_ref() + .or_not_found(lazy_format!("Manager for {}", package_id))? + .run_action(Guid::new(), action_id, input.unwrap_or_default()) .await - .as_package_data() - .as_idx(&pkg_id) - .or_not_found(&pkg_id)? - .as_installed() - .or_not_found(&pkg_id)? - .as_manifest() - .de()?; - - if let Some(action) = manifest.actions.0.get(&action_id) { - action - .execute( - &ctx, - &manifest.id, - &manifest.version, - &action_id, - &manifest.volumes, - input, - ) - .await - } else { - Err(Error::new( - eyre!("Action not found in manifest"), - crate::ErrorKind::NotFound, - )) - } } diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index a6ae2fff0..9085709ab 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -1,27 +1,49 @@ use std::collections::BTreeMap; -use std::marker::PhantomData; use chrono::{DateTime, Utc}; -use clap::ArgMatches; +use clap::Parser; use color_eyre::eyre::eyre; +use imbl_value::{json, InternedString}; +use itertools::Itertools; use josekit::jwk::Jwk; -use rpc_toolkit::command; -use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts}; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use sqlx::{Executor, Postgres}; use tracing::instrument; +use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::middleware::auth::{AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken}; -use crate::middleware::encrypt::EncryptedWire; +use crate::db::model::DatabaseModel; +use crate::middleware::auth::{ + AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes, +}; use crate::prelude::*; -use crate::util::display_none; -use crate::util::serde::{display_serializable, IoFormat}; +use crate::util::crypto::EncryptedWire; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::{ensure_code, Error, ResultExt}; -#[derive(Clone, Serialize, Deserialize)] + +#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] +#[ts(as = "BTreeMap::")] +pub struct Sessions(pub BTreeMap); +impl Sessions { + pub fn new() -> Self { + Self(BTreeMap::new()) + } +} +impl Map for Sessions { + type Key = InternedString; + type Value = Session; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone()) + } +} + +#[derive(Clone, Serialize, Deserialize, TS)] #[serde(untagged)] +#[ts(export)] pub enum PasswordType { EncryptedWire(EncryptedWire), String(String), @@ -61,20 +83,50 @@ impl std::str::FromStr for PasswordType { }) } } - -#[command(subcommands(login, logout, session, reset_password, get_pubkey))] -pub fn auth() -> Result<(), Error> { - Ok(()) -} - -pub fn cli_metadata() -> Value { - serde_json::json!({ - "platforms": ["cli"], - }) -} - -pub fn parse_metadata(_: &str, _: &ArgMatches) -> Result { - Ok(cli_metadata()) +pub fn auth() -> ParentHandler { + ParentHandler::new() + .subcommand( + "login", + from_fn_async(login_impl) + .with_metadata("login", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "login", + from_fn_async(cli_login) + .no_display() + .with_about("Log in to StartOS server"), + ) + .subcommand( + "logout", + from_fn_async(logout) + .with_metadata("get_session", Value::Bool(true)) + .no_display() + .with_about("Log out of StartOS server") + .with_call_remote::(), + ) + .subcommand( + "session", + session::().with_about("List or kill StartOS sessions"), + ) + .subcommand( + "reset-password", + from_fn_async(reset_password_impl).no_cli(), + ) + .subcommand( + "reset-password", + from_fn_async(cli_reset_password) + .no_display() + .with_about("Reset StartOS password"), + ) + .subcommand( + "get-pubkey", + from_fn_async(get_pubkey) + .with_metadata("authenticated", Value::Bool(false)) + .no_display() + .with_about("Get public key derived from server private key") + .with_call_remote::(), + ) } #[test] @@ -92,24 +144,25 @@ fn gen_pwd() { #[instrument(skip_all)] async fn cli_login( - ctx: CliContext, - password: Option, - metadata: Value, + HandlerArgs { + context: ctx, + parent_method, + method, + .. + }: HandlerArgs, ) -> Result<(), RpcError> { - let password = if let Some(password) = password { - password.decrypt(&ctx)? - } else { - rpassword::prompt_password("Password: ")? - }; - - rpc_toolkit::command_helpers::call_remote( - ctx, - "auth.login", - serde_json::json!({ "password": password, "metadata": metadata }), - PhantomData::<()>, + let password = rpassword::prompt_password("Password: ")?; + + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + json!({ + "password": password, + "metadata": { + "platforms": ["cli"], + }, + }), ) - .await? - .result?; + .await?; Ok(()) } @@ -128,99 +181,143 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> { Ok(()) } -pub async fn check_password_against_db(secrets: &mut Ex, password: &str) -> Result<(), Error> -where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, -{ - let pw_hash = sqlx::query!("SELECT password FROM account") - .fetch_one(secrets) - .await? - .password; +pub fn check_password_against_db(db: &DatabaseModel, password: &str) -> Result<(), Error> { + let pw_hash = db.as_private().as_password().de()?; check_password(&pw_hash, password)?; Ok(()) } -#[command( - custom_cli(cli_login(async, context(CliContext))), - display(display_none), - metadata(authenticated = false) -)] -#[instrument(skip_all)] -pub async fn login( - #[context] ctx: RpcContext, - #[request] req: &RequestParts, - #[response] res: &mut ResponseParts, - #[arg] password: Option, - #[arg( - parse(parse_metadata), - default = "cli_metadata", - help = "RPC Only: This value cannot be overidden from the cli" - )] +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct LoginParams { + password: Option, + #[ts(skip)] + #[serde(rename = "__auth_userAgent")] // from Auth middleware + user_agent: Option, + #[serde(default)] + ephemeral: bool, + #[serde(default)] + #[ts(type = "any")] metadata: Value, -) -> Result<(), Error> { - let password = password.unwrap_or_default().decrypt(&ctx)?; - let mut handle = ctx.secret_store.acquire().await?; - check_password_against_db(handle.as_mut(), &password).await?; - - let hash_token = HashSessionToken::new(); - let user_agent = req.headers.get("user-agent").and_then(|h| h.to_str().ok()); - let metadata = serde_json::to_string(&metadata).with_kind(crate::ErrorKind::Database)?; - let hash_token_hashed = hash_token.hashed(); - sqlx::query!( - "INSERT INTO session (id, user_agent, metadata) VALUES ($1, $2, $3)", - hash_token_hashed, +} + +#[instrument(skip_all)] +pub async fn login_impl( + ctx: RpcContext, + LoginParams { + password, user_agent, + ephemeral, metadata, - ) - .execute(handle.as_mut()) - .await?; - res.headers.insert( - "set-cookie", - hash_token.header_value()?, // Should be impossible, but don't want to panic - ); + }: LoginParams, +) -> Result { + let password = password.unwrap_or_default().decrypt(&ctx)?; - Ok(()) + if ephemeral { + check_password_against_db(&ctx.db.peek().await, &password)?; + let hash_token = HashSessionToken::new(); + ctx.ephemeral_sessions.mutate(|s| { + s.0.insert( + hash_token.hashed().clone(), + Session { + logged_in: Utc::now(), + last_active: Utc::now(), + user_agent, + metadata, + }, + ) + }); + Ok(hash_token.to_login_res()) + } else { + ctx.db + .mutate(|db| { + check_password_against_db(db, &password)?; + let hash_token = HashSessionToken::new(); + db.as_private_mut().as_sessions_mut().insert( + hash_token.hashed(), + &Session { + logged_in: Utc::now(), + last_active: Utc::now(), + user_agent, + metadata, + }, + )?; + + Ok(hash_token.to_login_res()) + }) + .await + } +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct LogoutParams { + #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware + session: InternedString, } -#[command(display(display_none), metadata(authenticated = false))] -#[instrument(skip_all)] pub async fn logout( - #[context] ctx: RpcContext, - #[request] req: &RequestParts, + ctx: RpcContext, + LogoutParams { session }: LogoutParams, ) -> Result, Error> { - let auth = match HashSessionToken::from_request_parts(req) { - Err(_) => return Ok(None), - Ok(a) => a, - }; - Ok(Some(HasLoggedOutSessions::new(vec![auth], &ctx).await?)) + Ok(Some( + HasLoggedOutSessions::new(vec![HashSessionToken::from_token(session)], &ctx).await?, + )) } -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] pub struct Session { - logged_in: DateTime, - last_active: DateTime, - user_agent: Option, - metadata: Value, + #[ts(type = "string")] + pub logged_in: DateTime, + #[ts(type = "string")] + pub last_active: DateTime, + #[ts(skip)] + pub user_agent: Option, + #[ts(type = "any")] + pub metadata: Value, } -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] pub struct SessionList { - current: String, - sessions: BTreeMap, + #[ts(type = "string | null")] + current: Option, + sessions: Sessions, } -#[command(subcommands(list, kill))] -pub async fn session() -> Result<(), Error> { - Ok(()) +pub fn session() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list) + .with_metadata("get_session", Value::Bool(true)) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_sessions(handle.params, result)) + }) + .with_about("Display all server sessions") + .with_call_remote::(), + ) + .subcommand( + "kill", + from_fn_async(kill) + .no_display() + .with_about("Terminate existing server session(s)") + .with_call_remote::(), + ) } -fn display_sessions(arg: SessionList, matches: &ArgMatches) { +fn display_sessions(params: WithIoFormat, arg: SessionList) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(arg, matches); + if let Some(format) = params.format { + return display_serializable(format, arg); } let mut table = Table::new(); @@ -231,7 +328,7 @@ fn display_sessions(arg: SessionList, matches: &ArgMatches) { "USER AGENT", "METADATA", ]); - for (id, session) in arg.sessions { + for (id, session) in arg.sessions.0 { let mut row = row![ &id, &format!("{}", session.logged_in), @@ -239,7 +336,7 @@ fn display_sessions(arg: SessionList, matches: &ArgMatches) { session.user_agent.as_deref().unwrap_or("N/A"), &format!("{}", session.metadata), ]; - if id == arg.current { + if Some(id) == arg.current { row.iter_mut() .map(|c| c.style(Attr::ForegroundColor(color::GREEN))) .collect::<()>() @@ -249,77 +346,82 @@ fn display_sessions(arg: SessionList, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_sessions))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ListParams { + #[arg(skip)] + #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware + session: Option, +} + +// #[command(display(display_sessions))] #[instrument(skip_all)] pub async fn list( - #[context] ctx: RpcContext, - #[request] req: &RequestParts, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, + ctx: RpcContext, + ListParams { session, .. }: ListParams, ) -> Result { + let mut sessions = ctx.db.peek().await.into_private().into_sessions().de()?; + ctx.ephemeral_sessions.peek(|s| { + sessions + .0 + .extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone()))) + }); Ok(SessionList { - current: HashSessionToken::from_request_parts(req)?.as_hash(), - sessions: sqlx::query!( - "SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP" - ) - .fetch_all(ctx.secret_store.acquire().await?.as_mut()) - .await? - .into_iter() - .map(|row| { - Ok(( - row.id, - Session { - logged_in: DateTime::from_utc(row.logged_in, Utc), - last_active: DateTime::from_utc(row.last_active, Utc), - user_agent: row.user_agent, - metadata: serde_json::from_str(&row.metadata) - .with_kind(crate::ErrorKind::Database)?, - }, - )) - }) - .collect::>()?, + current: session, + sessions, }) } -fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result, RpcError> { - Ok(arg.split(",").map(|s| s.trim().to_owned()).collect()) -} - #[derive(Debug, Clone, Serialize, Deserialize)] -struct KillSessionId(String); +struct KillSessionId(InternedString); + +impl KillSessionId { + fn new(id: String) -> Self { + Self(InternedString::from(id)) + } +} impl AsLogoutSessionId for KillSessionId { - fn as_logout_session_id(self) -> String { + fn as_logout_session_id(self) -> InternedString { self.0 } } -#[command(display(display_none))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct KillParams { + ids: Vec, +} + #[instrument(skip_all)] -pub async fn kill( - #[context] ctx: RpcContext, - #[arg(parse(parse_comma_separated))] ids: Vec, -) -> Result<(), Error> { - HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId), &ctx).await?; +pub async fn kill(ctx: RpcContext, KillParams { ids }: KillParams) -> Result<(), Error> { + HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?; Ok(()) } -#[instrument(skip_all)] -async fn cli_reset_password( - ctx: CliContext, +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ResetPasswordParams { old_password: Option, new_password: Option, +} + +#[instrument(skip_all)] +async fn cli_reset_password( + HandlerArgs { + context: ctx, + parent_method, + method, + .. + }: HandlerArgs, ) -> Result<(), RpcError> { - let old_password = if let Some(old_password) = old_password { - old_password.decrypt(&ctx)? - } else { - rpassword::prompt_password("Current Password: ")? - }; + let old_password = rpassword::prompt_password("Current Password: ")?; - let new_password = if let Some(new_password) = new_password { - new_password.decrypt(&ctx)? - } else { + let new_password = { let new_password = rpassword::prompt_password("New Password: ")?; if new_password != rpassword::prompt_password("Confirm: ")? { return Err(Error::new( @@ -331,28 +433,22 @@ async fn cli_reset_password( new_password }; - rpc_toolkit::command_helpers::call_remote( - ctx, - "auth.reset-password", - serde_json::json!({ "old-password": old_password, "new-password": new_password }), - PhantomData::<()>, + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + imbl_value::json!({ "old-password": old_password, "new-password": new_password }), ) - .await? - .result?; + .await?; Ok(()) } -#[command( - rename = "reset-password", - custom_cli(cli_reset_password(async, context(CliContext))), - display(display_none) -)] #[instrument(skip_all)] -pub async fn reset_password( - #[context] ctx: RpcContext, - #[arg(rename = "old-password")] old_password: Option, - #[arg(rename = "new-password")] new_password: Option, +pub async fn reset_password_impl( + ctx: RpcContext, + ResetPasswordParams { + old_password, + new_password, + }: ResetPasswordParams, ) -> Result<(), Error> { let old_password = old_password.unwrap_or_default().decrypt(&ctx)?; let new_password = new_password.unwrap_or_default().decrypt(&ctx)?; @@ -367,25 +463,24 @@ pub async fn reset_password( )); } account.set_password(&new_password)?; - account.save(&ctx.secret_store).await?; let account_password = &account.password; + let account = account.clone(); ctx.db .mutate(|d| { - d.as_server_info_mut() + d.as_public_mut() + .as_server_info_mut() .as_password_hash_mut() - .ser(account_password) + .ser(account_password)?; + account.save(d)?; + + Ok(()) }) .await } -#[command( - rename = "get-pubkey", - display(display_none), - metadata(authenticated = false) -)] #[instrument(skip_all)] -pub async fn get_pubkey(#[context] ctx: RpcContext) -> Result { - let secret = ctx.as_ref().clone(); +pub async fn get_pubkey(ctx: RpcContext) -> Result { + let secret = >::as_ref(&ctx).clone(); let pub_key = secret.to_public_key()?; Ok(pub_key) } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 21eedbaf2..136595b67 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -1,280 +1,313 @@ use std::collections::BTreeMap; -use std::panic::UnwindSafe; use std::path::{Path, PathBuf}; use std::sync::Arc; use chrono::Utc; -use clap::ArgMatches; +use clap::Parser; use color_eyre::eyre::eyre; use helpers::AtomicFile; use imbl::OrdSet; -use models::Version; -use rpc_toolkit::command; +use models::PackageId; +use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; -use tokio::sync::Mutex; use tracing::instrument; +use ts_rs::TS; -use super::target::BackupTargetId; +use super::target::{BackupTargetId, PackageBackupInfo}; use super::PackageBackupReport; use crate::auth::check_password_against_db; use crate::backup::os::OsBackup; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; -use crate::db::model::BackupProgress; -use crate::db::package::get_packages; +use crate::db::model::public::BackupProgress; +use crate::db::model::{Database, DatabaseModel}; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::TmpMountGuard; -use crate::manager::BackupReturn; -use crate::notifications::NotificationLevel; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; +use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::display_none; use crate::util::io::dir_copy; use crate::util::serde::IoFormat; use crate::version::VersionT; -fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result, Error> { - arg.split(',') - .map(|s| s.trim().parse::().map_err(Error::from)) - .collect() +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct BackupParams { + target_id: BackupTargetId, + #[arg(long = "old-password")] + old_password: Option, + #[arg(long = "package-ids")] + package_ids: Option>, + password: crate::auth::PasswordType, +} + +struct BackupStatusGuard(Option>); +impl BackupStatusGuard { + fn new(db: TypedPatchDb) -> Self { + Self(Some(db)) + } + async fn handle_result( + mut self, + result: Result, Error>, + ) -> Result<(), Error> { + if let Some(db) = self.0.as_ref() { + db.mutate(|v| { + v.as_public_mut() + .as_server_info_mut() + .as_status_info_mut() + .as_backup_progress_mut() + .ser(&None) + }) + .await?; + } + if let Some(db) = self.0.take() { + match result { + Ok(report) if report.iter().all(|(_, rep)| rep.error.is_none()) => { + db.mutate(|db| { + notify( + db, + None, + NotificationLevel::Success, + "Backup Complete".to_owned(), + "Your backup has completed".to_owned(), + BackupReport { + server: ServerBackupReport { + attempted: true, + error: None, + }, + packages: report, + }, + ) + }) + .await + } + Ok(report) => { + db.mutate(|db| { + notify( + db, + None, + NotificationLevel::Warning, + "Backup Complete".to_owned(), + "Your backup has completed, but some package(s) failed to backup" + .to_owned(), + BackupReport { + server: ServerBackupReport { + attempted: true, + error: None, + }, + packages: report, + }, + ) + }) + .await + } + Err(e) => { + tracing::error!("Backup Failed: {}", e); + tracing::debug!("{:?}", e); + let err_string = e.to_string(); + db.mutate(|db| { + notify( + db, + None, + NotificationLevel::Error, + "Backup Failed".to_owned(), + "Your backup failed to complete.".to_owned(), + BackupReport { + server: ServerBackupReport { + attempted: true, + error: Some(err_string), + }, + packages: BTreeMap::new(), + }, + ) + }) + .await + } + }?; + } + Ok(()) + } +} +impl Drop for BackupStatusGuard { + fn drop(&mut self) { + if let Some(db) = self.0.take() { + tokio::spawn(async move { + db.mutate(|v| { + v.as_public_mut() + .as_server_info_mut() + .as_status_info_mut() + .as_backup_progress_mut() + .ser(&None) + }) + .await + .log_err() + }); + } + } } -#[command(rename = "create", display(display_none))] #[instrument(skip(ctx, old_password, password))] pub async fn backup_all( - #[context] ctx: RpcContext, - #[arg(rename = "target-id")] target_id: BackupTargetId, - #[arg(rename = "old-password", long = "old-password")] old_password: Option< - crate::auth::PasswordType, - >, - #[arg( - rename = "package-ids", - long = "package-ids", - parse(parse_comma_separated) - )] - package_ids: Option>, - #[arg] password: crate::auth::PasswordType, + ctx: RpcContext, + BackupParams { + target_id, + old_password, + package_ids, + password, + }: BackupParams, ) -> Result<(), Error> { - let db = ctx.db.peek().await; let old_password_decrypted = old_password .as_ref() .unwrap_or(&password) .clone() .decrypt(&ctx)?; let password = password.decrypt(&ctx)?; - check_password_against_db(ctx.secret_store.acquire().await?.as_mut(), &password).await?; - let fs = target_id - .load(ctx.secret_store.acquire().await?.as_mut()) - .await?; + + let ((fs, package_ids, server_id), status_guard) = ( + ctx.db + .mutate(|db| { + check_password_against_db(db, &password)?; + let fs = target_id.load(db)?; + let package_ids = if let Some(ids) = package_ids { + ids.into_iter().collect() + } else { + db.as_public() + .as_package_data() + .as_entries()? + .into_iter() + .filter(|(_, m)| m.as_state_info().expect_installed().is_ok()) + .map(|(id, _)| id) + .collect() + }; + assure_backing_up(db, &package_ids)?; + Ok(( + fs, + package_ids, + db.as_public().as_server_info().as_id().de()?, + )) + }) + .await?, + BackupStatusGuard::new(ctx.db.clone()), + ); + let mut backup_guard = BackupMountGuard::mount( TmpMountGuard::mount(&fs, ReadWrite).await?, + &server_id, &old_password_decrypted, ) .await?; - let package_ids = if let Some(ids) = package_ids { - ids.into_iter() - .flat_map(|package_id| { - let version = db - .as_package_data() - .as_idx(&package_id)? - .as_manifest() - .as_version() - .de() - .ok()?; - Some((package_id, version)) - }) - .collect() - } else { - get_packages(db.clone())?.into_iter().collect() - }; if old_password.is_some() { backup_guard.change_password(&password)?; } - assure_backing_up(&ctx.db, &package_ids).await?; tokio::task::spawn(async move { - let backup_res = perform_backup(&ctx, backup_guard, &package_ids).await; - match backup_res { - Ok(report) if report.iter().all(|(_, rep)| rep.error.is_none()) => ctx - .notification_manager - .notify( - ctx.db.clone(), - None, - NotificationLevel::Success, - "Backup Complete".to_owned(), - "Your backup has completed".to_owned(), - BackupReport { - server: ServerBackupReport { - attempted: true, - error: None, - }, - packages: report - .into_iter() - .map(|((package_id, _), value)| (package_id, value)) - .collect(), - }, - None, - ) - .await - .expect("failed to send notification"), - Ok(report) => ctx - .notification_manager - .notify( - ctx.db.clone(), - None, - NotificationLevel::Warning, - "Backup Complete".to_owned(), - "Your backup has completed, but some package(s) failed to backup".to_owned(), - BackupReport { - server: ServerBackupReport { - attempted: true, - error: None, - }, - packages: report - .into_iter() - .map(|((package_id, _), value)| (package_id, value)) - .collect(), - }, - None, - ) - .await - .expect("failed to send notification"), - Err(e) => { - tracing::error!("Backup Failed: {}", e); - tracing::debug!("{:?}", e); - ctx.notification_manager - .notify( - ctx.db.clone(), - None, - NotificationLevel::Error, - "Backup Failed".to_owned(), - "Your backup failed to complete.".to_owned(), - BackupReport { - server: ServerBackupReport { - attempted: true, - error: Some(e.to_string()), - }, - packages: BTreeMap::new(), - }, - None, - ) - .await - .expect("failed to send notification"); - } - } - ctx.db - .mutate(|v| { - v.as_server_info_mut() - .as_status_info_mut() - .as_backup_progress_mut() - .ser(&None) - }) - .await?; - Ok::<(), Error>(()) + status_guard + .handle_result(perform_backup(&ctx, backup_guard, &package_ids).await) + .await + .unwrap(); }); Ok(()) } #[instrument(skip(db, packages))] -async fn assure_backing_up( - db: &PatchDb, - packages: impl IntoIterator + UnwindSafe + Send, +fn assure_backing_up<'a>( + db: &mut DatabaseModel, + packages: impl IntoIterator, ) -> Result<(), Error> { - db.mutate(|v| { - let backing_up = v - .as_server_info_mut() - .as_status_info_mut() - .as_backup_progress_mut(); - if backing_up - .clone() - .de()? - .iter() - .flat_map(|x| x.values()) - .fold(false, |acc, x| { - if !x.complete { - return true; - } - acc - }) - { - return Err(Error::new( - eyre!("Server is already backing up!"), - ErrorKind::InvalidRequest, - )); - } - backing_up.ser(&Some( - packages - .into_iter() - .map(|(x, _)| (x.clone(), BackupProgress { complete: false })) - .collect(), - ))?; - Ok(()) - }) - .await + let backing_up = db + .as_public_mut() + .as_server_info_mut() + .as_status_info_mut() + .as_backup_progress_mut(); + if backing_up + .clone() + .de()? + .iter() + .flat_map(|x| x.values()) + .fold(false, |acc, x| { + if !x.complete { + return true; + } + acc + }) + { + return Err(Error::new( + eyre!("Server is already backing up!"), + ErrorKind::InvalidRequest, + )); + } + backing_up.ser(&Some( + packages + .into_iter() + .map(|x| (x.clone(), BackupProgress { complete: false })) + .collect(), + ))?; + Ok(()) } #[instrument(skip(ctx, backup_guard))] async fn perform_backup( ctx: &RpcContext, backup_guard: BackupMountGuard, - package_ids: &OrdSet<(PackageId, Version)>, -) -> Result, Error> { + package_ids: &OrdSet, +) -> Result, Error> { + let db = ctx.db.peek().await; let mut backup_report = BTreeMap::new(); - let backup_guard = Arc::new(Mutex::new(backup_guard)); + let backup_guard = Arc::new(backup_guard); + let mut package_backups: BTreeMap = + backup_guard.metadata.package_backups.clone(); - for package_id in package_ids { - let (response, _report) = match ctx - .managers - .get(package_id) - .await - .ok_or_else(|| Error::new(eyre!("Manager not found"), ErrorKind::InvalidRequest))? - .backup(backup_guard.clone()) - .await - { - BackupReturn::Ran { report, res } => (res, report), - BackupReturn::AlreadyRunning(report) => { - backup_report.insert(package_id.clone(), report); - continue; - } - BackupReturn::Error(error) => { - tracing::warn!("Backup thread error"); - tracing::debug!("{error:?}"); - backup_report.insert( - package_id.clone(), - PackageBackupReport { - error: Some("Backup thread error".to_owned()), + for id in package_ids { + if let Some(service) = &*ctx.services.get(id).await { + let backup_result = service + .backup(backup_guard.package_backup(id).await?) + .await + .err() + .map(|e| e.to_string()); + if backup_result.is_none() { + let manifest = db + .as_public() + .as_package_data() + .as_idx(id) + .or_not_found(id)? + .as_state_info() + .expect_installed()? + .as_manifest(); + + package_backups.insert( + id.clone(), + PackageBackupInfo { + os_version: manifest.as_os_version().de()?, + version: manifest.as_version().de()?, + title: manifest.as_title().de()?, + timestamp: Utc::now(), }, ); - continue; } - }; - backup_report.insert( - package_id.clone(), - PackageBackupReport { - error: response.as_ref().err().map(|e| e.to_string()), - }, - ); - - if let Ok(pkg_meta) = response { - backup_guard - .lock() - .await - .metadata - .package_backups - .insert(package_id.0.clone(), pkg_meta); + backup_report.insert( + id.clone(), + PackageBackupReport { + error: backup_result, + }, + ); } } - let ui = ctx.db.peek().await.into_ui().de()?; + let mut backup_guard = Arc::try_unwrap(backup_guard).map_err(|_| { + Error::new( + eyre!("leaked reference to BackupMountGuard"), + ErrorKind::Incoherent, + ) + })?; - let mut os_backup_file = AtomicFile::new( - backup_guard.lock().await.as_ref().join("os-backup.cbor"), - None::, - ) - .await - .with_kind(ErrorKind::Filesystem)?; + let ui = ctx.db.peek().await.into_public().into_ui().de()?; + + let mut os_backup_file = + AtomicFile::new(backup_guard.path().join("os-backup.json"), None::) + .await + .with_kind(ErrorKind::Filesystem)?; os_backup_file - .write_all(&IoFormat::Cbor.to_vec(&OsBackup { + .write_all(&IoFormat::Json.to_vec(&OsBackup { account: ctx.account.read().await.clone(), ui, })?) @@ -284,38 +317,37 @@ async fn perform_backup( .await .with_kind(ErrorKind::Filesystem)?; - let luks_folder_old = backup_guard.lock().await.as_ref().join("luks.old"); + let luks_folder_old = backup_guard.path().join("luks.old"); if tokio::fs::metadata(&luks_folder_old).await.is_ok() { tokio::fs::remove_dir_all(&luks_folder_old).await?; } - let luks_folder_bak = backup_guard.lock().await.as_ref().join("luks"); + let luks_folder_bak = backup_guard.path().join("luks"); if tokio::fs::metadata(&luks_folder_bak).await.is_ok() { tokio::fs::rename(&luks_folder_bak, &luks_folder_old).await?; } - let luks_folder = Path::new("/media/embassy/config/luks"); + let luks_folder = Path::new("/media/startos/config/luks"); if tokio::fs::metadata(&luks_folder).await.is_ok() { - dir_copy(&luks_folder, &luks_folder_bak, None).await?; + dir_copy(luks_folder, &luks_folder_bak, None).await?; } - let timestamp = Some(Utc::now()); - let mut backup_guard = Arc::try_unwrap(backup_guard) - .map_err(|_err| { - Error::new( - eyre!("Backup guard could not ensure that the others where dropped"), - ErrorKind::Unknown, - ) - })? - .into_inner(); + let timestamp = Utc::now(); - backup_guard.unencrypted_metadata.version = crate::version::Current::new().semver().into(); - backup_guard.unencrypted_metadata.full = true; - backup_guard.metadata.version = crate::version::Current::new().semver().into(); - backup_guard.metadata.timestamp = timestamp; + backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into(); + backup_guard.unencrypted_metadata.hostname = ctx.account.read().await.hostname.clone(); + backup_guard.unencrypted_metadata.timestamp = timestamp.clone(); + backup_guard.metadata.version = crate::version::Current::default().semver().into(); + backup_guard.metadata.timestamp = Some(timestamp); + backup_guard.metadata.package_backups = package_backups; backup_guard.save_and_unmount().await?; ctx.db - .mutate(|v| v.as_server_info_mut().as_last_backup_mut().ser(×tamp)) + .mutate(|v| { + v.as_public_mut() + .as_server_info_mut() + .as_last_backup_mut() + .ser(&Some(timestamp)) + }) .await?; Ok(backup_report) diff --git a/core/startos/src/backup/mod.rs b/core/startos/src/backup/mod.rs index 2f3f9bd8f..110a918b6 100644 --- a/core/startos/src/backup/mod.rs +++ b/core/startos/src/backup/mod.rs @@ -1,33 +1,15 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::collections::BTreeMap; use chrono::{DateTime, Utc}; -use color_eyre::eyre::eyre; -use helpers::AtomicFile; -use models::{ImageId, OptionExt}; +use models::{HostId, PackageId}; use reqwest::Url; -use rpc_toolkit::command; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; -use tracing::instrument; -use self::target::PackageBackupInfo; -use crate::context::RpcContext; -use crate::install::PKG_ARCHIVE_DIR; -use crate::manager::manager_seed::ManagerSeed; -use crate::net::interface::InterfaceId; -use crate::net::keys::Key; +use crate::context::CliContext; +#[allow(unused_imports)] use crate::prelude::*; -use crate::procedure::docker::DockerContainers; -use crate::procedure::{NoOutput, PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::{Base32, Base64, IoFormat}; -use crate::util::Version; -use crate::version::{Current, VersionT}; -use crate::volume::{backup_dir, Volume, VolumeId, Volumes, BACKUP_DIR}; -use crate::{Error, ErrorKind, ResultExt}; +use crate::util::serde::{Base32, Base64}; pub mod backup_bulk; pub mod os; @@ -51,176 +33,38 @@ pub struct PackageBackupReport { pub error: Option, } -#[command(subcommands(backup_bulk::backup_all, target::target))] -pub fn backup() -> Result<(), Error> { - Ok(()) +// #[command(subcommands(backup_bulk::backup_all, target::target))] +pub fn backup() -> ParentHandler { + ParentHandler::new() + .subcommand( + "create", + from_fn_async(backup_bulk::backup_all) + .no_display() + .with_about("Create backup for all packages") + .with_call_remote::(), + ) + .subcommand( + "target", + target::target::().with_about("Commands related to a backup target"), + ) } -#[command(rename = "backup", subcommands(restore::restore_packages_rpc))] -pub fn package_backup() -> Result<(), Error> { - Ok(()) +pub fn package_backup() -> ParentHandler { + ParentHandler::new().subcommand( + "restore", + from_fn_async(restore::restore_packages_rpc) + .no_display() + .with_about("Restore package(s) from backup") + .with_call_remote::(), + ) } #[derive(Deserialize, Serialize)] struct BackupMetadata { pub timestamp: DateTime, #[serde(default)] - pub network_keys: BTreeMap>, + pub network_keys: BTreeMap>, #[serde(default)] - pub tor_keys: BTreeMap>, // DEPRECATED - pub marketplace_url: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct BackupActions { - pub create: PackageProcedure, - pub restore: PackageProcedure, -} -impl BackupActions { - pub fn validate( - &self, - _container: &Option, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - ) -> Result<(), Error> { - self.create - .validate(eos_version, volumes, image_ids, false) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Create"))?; - self.restore - .validate(eos_version, volumes, image_ids, false) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Restore"))?; - Ok(()) - } - - #[instrument(skip_all)] - pub async fn create(&self, seed: Arc) -> Result { - let manifest = &seed.manifest; - let mut volumes = seed.manifest.volumes.to_readonly(); - let ctx = &seed.ctx; - let pkg_id = &manifest.id; - let pkg_version = &manifest.version; - volumes.insert(VolumeId::Backup, Volume::Backup { readonly: false }); - let backup_dir = backup_dir(&manifest.id); - if tokio::fs::metadata(&backup_dir).await.is_err() { - tokio::fs::create_dir_all(&backup_dir).await? - } - self.create - .execute::<(), NoOutput>( - ctx, - pkg_id, - pkg_version, - ProcedureName::CreateBackup, - &volumes, - None, - None, - ) - .await? - .map_err(|e| eyre!("{}", e.1)) - .with_kind(crate::ErrorKind::Backup)?; - let (network_keys, tor_keys): (Vec<_>, Vec<_>) = - Key::for_package(&ctx.secret_store, pkg_id) - .await? - .into_iter() - .filter_map(|k| { - let interface = k.interface().map(|(_, i)| i)?; - Some(( - (interface.clone(), Base64(k.as_bytes())), - (interface, Base32(k.tor_key().as_bytes())), - )) - }) - .unzip(); - let marketplace_url = ctx - .db - .peek() - .await - .as_package_data() - .as_idx(&pkg_id) - .or_not_found(pkg_id)? - .expect_as_installed()? - .as_installed() - .as_marketplace_url() - .de()?; - let tmp_path = Path::new(BACKUP_DIR) - .join(pkg_id) - .join(format!("{}.s9pk", pkg_id)); - let s9pk_path = ctx - .datadir - .join(PKG_ARCHIVE_DIR) - .join(pkg_id) - .join(pkg_version.as_str()) - .join(format!("{}.s9pk", pkg_id)); - let mut infile = File::open(&s9pk_path).await?; - let mut outfile = AtomicFile::new(&tmp_path, None::) - .await - .with_kind(ErrorKind::Filesystem)?; - tokio::io::copy(&mut infile, &mut *outfile) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("cp {} -> {}", s9pk_path.display(), tmp_path.display()), - ) - })?; - outfile.save().await.with_kind(ErrorKind::Filesystem)?; - let timestamp = Utc::now(); - let metadata_path = Path::new(BACKUP_DIR).join(pkg_id).join("metadata.cbor"); - let mut outfile = AtomicFile::new(&metadata_path, None::) - .await - .with_kind(ErrorKind::Filesystem)?; - let network_keys = network_keys.into_iter().collect(); - let tor_keys = tor_keys.into_iter().collect(); - outfile - .write_all(&IoFormat::Cbor.to_vec(&BackupMetadata { - timestamp, - network_keys, - tor_keys, - marketplace_url, - })?) - .await?; - outfile.save().await.with_kind(ErrorKind::Filesystem)?; - Ok(PackageBackupInfo { - os_version: Current::new().semver().into(), - title: manifest.title.clone(), - version: pkg_version.clone(), - timestamp, - }) - } - - #[instrument(skip_all)] - pub async fn restore( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result, Error> { - let mut volumes = volumes.clone(); - volumes.insert(VolumeId::Backup, Volume::Backup { readonly: true }); - self.restore - .execute::<(), NoOutput>( - ctx, - pkg_id, - pkg_version, - ProcedureName::RestoreBackup, - &volumes, - None, - None, - ) - .await? - .map_err(|e| eyre!("{}", e.1)) - .with_kind(crate::ErrorKind::Restore)?; - let metadata_path = Path::new(BACKUP_DIR).join(pkg_id).join("metadata.cbor"); - let metadata: BackupMetadata = IoFormat::Cbor.from_slice( - &tokio::fs::read(&metadata_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - metadata_path.display().to_string(), - ) - })?, - )?; - - Ok(metadata.marketplace_url) - } + pub tor_keys: BTreeMap>, // DEPRECATED + pub registry: Option, } diff --git a/core/startos/src/backup/os.rs b/core/startos/src/backup/os.rs index 5ab8bd12e..324543fb8 100644 --- a/core/startos/src/backup/os.rs +++ b/core/startos/src/backup/os.rs @@ -1,13 +1,16 @@ -use openssl::pkey::PKey; +use imbl_value::InternedString; +use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use patch_db::Value; use serde::{Deserialize, Serialize}; +use ssh_key::private::Ed25519Keypair; +use torut::onion::TorSecretKeyV3; use crate::account::AccountInfo; use crate::hostname::{generate_hostname, generate_id, Hostname}; -use crate::net::keys::Key; use crate::prelude::*; -use crate::util::serde::Base64; +use crate::util::crypto::ed25519_expand_key; +use crate::util::serde::{Base32, Base64, Pem}; pub struct OsBackup { pub account: AccountInfo, @@ -19,19 +22,23 @@ impl<'de> Deserialize<'de> for OsBackup { D: serde::Deserializer<'de>, { let tagged = OsBackupSerDe::deserialize(deserializer)?; - match tagged.version { + Ok(match tagged.version { 0 => patch_db::value::from_value::(tagged.rest) .map_err(serde::de::Error::custom)? .project() - .map_err(serde::de::Error::custom), + .map_err(serde::de::Error::custom)?, 1 => patch_db::value::from_value::(tagged.rest) .map_err(serde::de::Error::custom)? - .project() - .map_err(serde::de::Error::custom), - v => Err(serde::de::Error::custom(&format!( - "Unknown backup version {v}" - ))), - } + .project(), + 2 => patch_db::value::from_value::(tagged.rest) + .map_err(serde::de::Error::custom)? + .project(), + v => { + return Err(serde::de::Error::custom(&format!( + "Unknown backup version {v}" + ))) + } + }) } } impl Serialize for OsBackup { @@ -40,11 +47,9 @@ impl Serialize for OsBackup { S: serde::Serializer, { OsBackupSerDe { - version: 1, - rest: patch_db::value::to_value( - &OsBackupV1::unproject(self).map_err(serde::ser::Error::custom)?, - ) - .map_err(serde::ser::Error::custom)?, + version: 2, + rest: patch_db::value::to_value(&OsBackupV2::unproject(self)) + .map_err(serde::ser::Error::custom)?, } .serialize(serializer) } @@ -62,10 +67,10 @@ struct OsBackupSerDe { #[derive(Deserialize)] #[serde(rename = "kebab-case")] struct OsBackupV0 { - // tor_key: Base32<[u8; 64]>, - root_ca_key: String, // PEM Encoded OpenSSL Key - root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate - ui: Value, // JSON Value + tor_key: Base32<[u8; 64]>, // Base32 Encoded Ed25519 Expanded Secret Key + root_ca_key: Pem>, // PEM Encoded OpenSSL Key + root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate + ui: Value, // JSON Value } impl OsBackupV0 { fn project(self) -> Result { @@ -74,9 +79,14 @@ impl OsBackupV0 { server_id: generate_id(), hostname: generate_hostname(), password: Default::default(), - key: Key::new(None), - root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?, - root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?, + root_ca_key: self.root_ca_key.0, + root_ca_cert: self.root_ca_cert.0, + ssh_key: ssh_key::PrivateKey::random( + &mut rand::thread_rng(), + ssh_key::Algorithm::Ed25519, + )?, + tor_keys: vec![TorSecretKeyV3::from(self.tor_key.0)], + compat_s9pk_key: ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()), }, ui: self.ui, }) @@ -87,36 +97,71 @@ impl OsBackupV0 { #[derive(Deserialize, Serialize)] #[serde(rename = "kebab-case")] struct OsBackupV1 { - server_id: String, // uuidv4 - hostname: String, // embassy-- - net_key: Base64<[u8; 32]>, // Ed25519 Secret Key - root_ca_key: String, // PEM Encoded OpenSSL Key - root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate - ui: Value, // JSON Value - // TODO add more + server_id: String, // uuidv4 + hostname: InternedString, // embassy-- + net_key: Base64<[u8; 32]>, // Ed25519 Secret Key + root_ca_key: Pem>, // PEM Encoded OpenSSL Key + root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate + ui: Value, // JSON Value } impl OsBackupV1 { - fn project(self) -> Result { - Ok(OsBackup { + fn project(self) -> OsBackup { + OsBackup { account: AccountInfo { server_id: self.server_id, hostname: Hostname(self.hostname), password: Default::default(), - key: Key::from_bytes(None, self.net_key.0), - root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?, - root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?, + root_ca_key: self.root_ca_key.0, + root_ca_cert: self.root_ca_cert.0, + ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)), + tor_keys: vec![TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0))], + compat_s9pk_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), }, ui: self.ui, - }) + } + } +} + +/// V2 +#[derive(Deserialize, Serialize)] +#[serde(rename = "kebab-case")] + +struct OsBackupV2 { + server_id: String, // uuidv4 + hostname: InternedString, // - + root_ca_key: Pem>, // PEM Encoded OpenSSL Key + root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate + ssh_key: Pem, // PEM Encoded OpenSSH Key + tor_keys: Vec, // Base64 Encoded Ed25519 Expanded Secret Key + compat_s9pk_key: Pem, // PEM Encoded ED25519 Key + ui: Value, // JSON Value +} +impl OsBackupV2 { + fn project(self) -> OsBackup { + OsBackup { + account: AccountInfo { + server_id: self.server_id, + hostname: Hostname(self.hostname), + password: Default::default(), + root_ca_key: self.root_ca_key.0, + root_ca_cert: self.root_ca_cert.0, + ssh_key: self.ssh_key.0, + tor_keys: self.tor_keys, + compat_s9pk_key: self.compat_s9pk_key.0, + }, + ui: self.ui, + } } - fn unproject(backup: &OsBackup) -> Result { - Ok(Self { + fn unproject(backup: &OsBackup) -> Self { + Self { server_id: backup.account.server_id.clone(), hostname: backup.account.hostname.0.clone(), - net_key: Base64(backup.account.key.as_bytes()), - root_ca_key: String::from_utf8(backup.account.root_ca_key.private_key_to_pem_pkcs8()?)?, - root_ca_cert: String::from_utf8(backup.account.root_ca_cert.to_pem()?)?, + root_ca_key: Pem(backup.account.root_ca_key.clone()), + root_ca_cert: Pem(backup.account.root_ca_cert.clone()), + ssh_key: Pem(backup.account.ssh_key.clone()), + tor_keys: backup.account.tor_keys.clone(), + compat_s9pk_key: Pem(backup.account.compat_s9pk_key.clone()), ui: backup.ui.clone(), - }) + } } } diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index b72b319e2..21f154b60 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -1,461 +1,177 @@ use std::collections::BTreeMap; -use std::path::Path; -use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::Duration; -use clap::ArgMatches; -use futures::future::BoxFuture; -use futures::{stream, FutureExt, StreamExt}; -use openssl::x509::X509; -use rpc_toolkit::command; -use sqlx::Connection; -use tokio::fs::File; -use torut::onion::OnionAddressV3; +use clap::Parser; +use futures::{stream, StreamExt}; +use models::PackageId; +use patch_db::json_ptr::ROOT; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; use tracing::instrument; +use ts_rs::TS; use super::target::BackupTargetId; use crate::backup::os::OsBackup; -use crate::backup::BackupMetadata; -use crate::context::rpc::RpcContextConfig; +use crate::context::setup::SetupResult; use crate::context::{RpcContext, SetupContext}; -use crate::db::model::{PackageDataEntry, PackageDataEntryRestoring, StaticFiles}; -use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard}; +use crate::db::model::Database; +use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::TmpMountGuard; -use crate::hostname::Hostname; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::init::init; -use crate::install::progress::InstallProgress; -use crate::install::{download_install_s9pk, PKG_PUBLIC_DIR}; -use crate::notifications::NotificationLevel; use crate::prelude::*; -use crate::s9pk::manifest::{Manifest, PackageId}; -use crate::s9pk::reader::S9pkReader; -use crate::setup::SetupStatus; -use crate::util::display_none; -use crate::util::io::dir_size; +use crate::s9pk::S9pk; +use crate::service::service_map::DownloadInstallFuture; +use crate::setup::SetupExecuteProgress; use crate::util::serde::IoFormat; -use crate::volume::{backup_dir, BACKUP_DIR, PKG_VOLUME_DIR}; -fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result, Error> { - arg.split(',') - .map(|s| s.trim().parse().map_err(Error::from)) - .collect() +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct RestorePackageParams { + pub ids: Vec, + pub target_id: BackupTargetId, + pub password: String, } -#[command(rename = "restore", display(display_none))] +// #[command(rename = "restore", display(display_none))] #[instrument(skip(ctx, password))] pub async fn restore_packages_rpc( - #[context] ctx: RpcContext, - #[arg(parse(parse_comma_separated))] ids: Vec, - #[arg(rename = "target-id")] target_id: BackupTargetId, - #[arg] password: String, + ctx: RpcContext, + RestorePackageParams { + ids, + target_id, + password, + }: RestorePackageParams, ) -> Result<(), Error> { - let fs = target_id - .load(ctx.secret_store.acquire().await?.as_mut()) - .await?; - let backup_guard = - BackupMountGuard::mount(TmpMountGuard::mount(&fs, ReadWrite).await?, &password).await?; + let peek = ctx.db.peek().await; + let fs = target_id.load(&peek)?; + let backup_guard = BackupMountGuard::mount( + TmpMountGuard::mount(&fs, ReadWrite).await?, + &peek.as_public().as_server_info().as_id().de()?, + &password, + ) + .await?; - let (backup_guard, tasks, _) = restore_packages(&ctx, backup_guard, ids).await?; + let tasks = restore_packages(&ctx, backup_guard, ids).await?; tokio::spawn(async move { - stream::iter(tasks.into_iter().map(|x| (x, ctx.clone()))) - .for_each_concurrent(5, |(res, ctx)| async move { - match res.await { - (Ok(_), _) => (), - (Err(err), package_id) => { - if let Err(err) = ctx - .notification_manager - .notify( - ctx.db.clone(), - Some(package_id.clone()), - NotificationLevel::Error, - "Restoration Failure".to_string(), - format!("Error restoring package {}: {}", package_id, err), - (), - None, - ) - .await - { - tracing::error!("Failed to notify: {}", err); - tracing::debug!("{:?}", err); - }; - tracing::error!("Error restoring package {}: {}", package_id, err); + stream::iter(tasks) + .for_each_concurrent(5, |(id, res)| async move { + match async { res.await?.await }.await { + Ok(_) => (), + Err(err) => { + tracing::error!("Error restoring package {}: {}", id, err); tracing::debug!("{:?}", err); } } }) .await; - if let Err(e) = backup_guard.unmount().await { - tracing::error!("Error unmounting backup drive: {}", e); - tracing::debug!("{:?}", e); - } }); Ok(()) } -async fn approximate_progress( - rpc_ctx: &RpcContext, - progress: &mut ProgressInfo, -) -> Result<(), Error> { - for (id, size) in &mut progress.target_volume_size { - let dir = rpc_ctx.datadir.join(PKG_VOLUME_DIR).join(id).join("data"); - if tokio::fs::metadata(&dir).await.is_err() { - *size = 0; - } else { - *size = dir_size(&dir, None).await?; - } - } - Ok(()) -} - -async fn approximate_progress_loop( - ctx: &SetupContext, - rpc_ctx: &RpcContext, - mut starting_info: ProgressInfo, -) { - loop { - if let Err(e) = approximate_progress(rpc_ctx, &mut starting_info).await { - tracing::error!("Failed to approximate restore progress: {}", e); - tracing::debug!("{:?}", e); - } else { - *ctx.setup_status.write().await = Some(Ok(starting_info.flatten())); - } - tokio::time::sleep(Duration::from_secs(1)).await; - } -} - -#[derive(Debug, Default)] -struct ProgressInfo { - package_installs: BTreeMap>, - src_volume_size: BTreeMap, - target_volume_size: BTreeMap, -} -impl ProgressInfo { - fn flatten(&self) -> SetupStatus { - let mut total_bytes = 0; - let mut bytes_transferred = 0; - - for progress in self.package_installs.values() { - total_bytes += ((progress.size.unwrap_or(0) as f64) * 2.2) as u64; - bytes_transferred += progress.downloaded.load(Ordering::SeqCst); - bytes_transferred += ((progress.validated.load(Ordering::SeqCst) as f64) * 0.2) as u64; - bytes_transferred += progress.unpacked.load(Ordering::SeqCst); - } - - for size in self.src_volume_size.values() { - total_bytes += *size; - } - - for size in self.target_volume_size.values() { - bytes_transferred += *size; - } - - if bytes_transferred > total_bytes { - bytes_transferred = total_bytes; - } - - SetupStatus { - total_bytes: Some(total_bytes), - bytes_transferred, - complete: false, - } - } -} - -#[instrument(skip(ctx))] +#[instrument(skip_all)] pub async fn recover_full_embassy( - ctx: SetupContext, + ctx: &SetupContext, disk_guid: Arc, - embassy_password: String, + start_os_password: String, recovery_source: TmpMountGuard, - recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { - let backup_guard = BackupMountGuard::mount( - recovery_source, - recovery_password.as_deref().unwrap_or_default(), - ) - .await?; + server_id: &str, + recovery_password: &str, + SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { + let mut restore_phase = restore_phase.or_not_found("restore progress")?; - let os_backup_path = backup_guard.as_ref().join("os-backup.cbor"); - let mut os_backup: OsBackup = IoFormat::Cbor.from_slice( + let backup_guard = + BackupMountGuard::mount(recovery_source, server_id, recovery_password).await?; + + let os_backup_path = backup_guard.path().join("os-backup.json"); + let mut os_backup: OsBackup = IoFormat::Json.from_slice( &tokio::fs::read(&os_backup_path) .await .with_ctx(|_| (ErrorKind::Filesystem, os_backup_path.display().to_string()))?, )?; os_backup.account.password = argon2::hash_encoded( - embassy_password.as_bytes(), + start_os_password.as_bytes(), &rand::random::<[u8; 16]>()[..], &argon2::Config::rfc9106_low_mem(), ) .with_kind(ErrorKind::PasswordHashGeneration)?; - let secret_store = ctx.secret_store().await?; - - os_backup.account.save(&secret_store).await?; - - secret_store.close().await; - - let cfg = RpcContextConfig::load(ctx.config_path.clone()).await?; + let db = ctx.db().await?; + db.put(&ROOT, &Database::init(&os_backup.account)?).await?; + drop(db); - init(&cfg).await?; + let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?; - let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid.clone()).await?; + let rpc_ctx = RpcContext::init( + &ctx.webserver, + &ctx.config, + disk_guid.clone(), + Some(init_result), + rpc_ctx_phases, + ) + .await?; + restore_phase.start(); let ids: Vec<_> = backup_guard .metadata .package_backups .keys() .cloned() .collect(); - let (backup_guard, tasks, progress_info) = - restore_packages(&rpc_ctx, backup_guard, ids).await?; - let task_consumer_rpc_ctx = rpc_ctx.clone(); - tokio::select! { - _ = async move { - stream::iter(tasks.into_iter().map(|x| (x, task_consumer_rpc_ctx.clone()))) - .for_each_concurrent(5, |(res, ctx)| async move { - match res.await { - (Ok(_), _) => (), - (Err(err), package_id) => { - if let Err(err) = ctx.notification_manager.notify( - ctx.db.clone(), - Some(package_id.clone()), - NotificationLevel::Error, - "Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{ - tracing::error!("Failed to notify: {}", err); - tracing::debug!("{:?}", err); - }; - tracing::error!("Error restoring package {}: {}", package_id, err); - tracing::debug!("{:?}", err); - }, - } - }).await; - - } => { - - }, - _ = approximate_progress_loop(&ctx, &rpc_ctx, progress_info) => unreachable!(concat!(module_path!(), "::approximate_progress_loop should not terminate")), - } - - backup_guard.unmount().await?; - rpc_ctx.shutdown().await?; - - Ok(( - disk_guid, - os_backup.account.hostname, - os_backup.account.key.tor_address(), - os_backup.account.root_ca_cert, - )) -} - -#[instrument(skip(ctx, backup_guard))] -async fn restore_packages( - ctx: &RpcContext, - backup_guard: BackupMountGuard, - ids: Vec, -) -> Result< - ( - BackupMountGuard, - Vec, PackageId)>>, - ProgressInfo, - ), - Error, -> { - let guards = assure_restoring(ctx, ids, &backup_guard).await?; - - let mut progress_info = ProgressInfo::default(); - - let mut tasks = Vec::with_capacity(guards.len()); - for (manifest, guard) in guards { - let id = manifest.id.clone(); - let (progress, task) = restore_package(ctx.clone(), manifest, guard).await?; - progress_info - .package_installs - .insert(id.clone(), progress.clone()); - progress_info - .src_volume_size - .insert(id.clone(), dir_size(backup_dir(&id), None).await?); - progress_info.target_volume_size.insert(id.clone(), 0); - let package_id = id.clone(); - tasks.push( + let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?; + restore_phase.set_total(tasks.len() as u64); + let restore_phase = Arc::new(Mutex::new(restore_phase)); + stream::iter(tasks) + .for_each_concurrent(5, |(id, res)| { + let restore_phase = restore_phase.clone(); async move { - if let Err(e) = task.await { - tracing::error!("Error restoring package {}: {}", id, e); - tracing::debug!("{:?}", e); - Err(e) - } else { - Ok(()) + match async { res.await?.await }.await { + Ok(_) => (), + Err(err) => { + tracing::error!("Error restoring package {}: {}", id, err); + tracing::debug!("{:?}", err); + } } + *restore_phase.lock().await += 1; } - .map(|x| (x, package_id)) - .boxed(), - ); - } + }) + .await; + restore_phase.lock().await.complete(); - Ok((backup_guard, tasks, progress_info)) + Ok(((&os_backup.account).try_into()?, rpc_ctx)) } #[instrument(skip(ctx, backup_guard))] -async fn assure_restoring( +async fn restore_packages( ctx: &RpcContext, + backup_guard: BackupMountGuard, ids: Vec, - backup_guard: &BackupMountGuard, -) -> Result, Error> { - let mut guards = Vec::with_capacity(ids.len()); - - let mut insert_packages = BTreeMap::new(); - +) -> Result, Error> { + let backup_guard = Arc::new(backup_guard); + let mut tasks = BTreeMap::new(); for id in ids { - let peek = ctx.db.peek().await; - - let model = peek.as_package_data().as_idx(&id); - - if !model.is_none() { - return Err(Error::new( - eyre!("Can't restore over existing package: {}", id), - crate::ErrorKind::InvalidRequest, - )); - } - let guard = backup_guard.mount_package_backup(&id).await?; - let s9pk_path = Path::new(BACKUP_DIR).join(&id).join(format!("{}.s9pk", id)); - let mut rdr = S9pkReader::open(&s9pk_path, false).await?; - - let manifest = rdr.manifest().await?; - let version = manifest.version.clone(); - let progress = Arc::new(InstallProgress::new(Some( - tokio::fs::metadata(&s9pk_path).await?.len(), - ))); - - let public_dir_path = ctx - .datadir - .join(PKG_PUBLIC_DIR) - .join(&id) - .join(version.as_str()); - tokio::fs::create_dir_all(&public_dir_path).await?; - - let license_path = public_dir_path.join("LICENSE.md"); - let mut dst = File::create(&license_path).await?; - tokio::io::copy(&mut rdr.license().await?, &mut dst).await?; - dst.sync_all().await?; - - let instructions_path = public_dir_path.join("INSTRUCTIONS.md"); - let mut dst = File::create(&instructions_path).await?; - tokio::io::copy(&mut rdr.instructions().await?, &mut dst).await?; - dst.sync_all().await?; - - let icon_path = Path::new("icon").with_extension(&manifest.assets.icon_type()); - let icon_path = public_dir_path.join(&icon_path); - let mut dst = File::create(&icon_path).await?; - tokio::io::copy(&mut rdr.icon().await?, &mut dst).await?; - dst.sync_all().await?; - insert_packages.insert( - id.clone(), - PackageDataEntry::Restoring(PackageDataEntryRestoring { - install_progress: progress.clone(), - static_files: StaticFiles::local(&id, &version, manifest.assets.icon_type()), - manifest: manifest.clone(), - }), - ); - - guards.push((manifest, guard)); - } - ctx.db - .mutate(|db| { - for (id, package) in insert_packages { - db.as_package_data_mut().insert(&id, &package)?; - } - Ok(()) - }) - .await?; - Ok(guards) -} - -#[instrument(skip(ctx, guard))] -async fn restore_package<'a>( - ctx: RpcContext, - manifest: Manifest, - guard: PackageBackupMountGuard, -) -> Result<(Arc, BoxFuture<'static, Result<(), Error>>), Error> { - let id = manifest.id.clone(); - let s9pk_path = Path::new(BACKUP_DIR) - .join(&manifest.id) - .join(format!("{}.s9pk", id)); - - let metadata_path = Path::new(BACKUP_DIR).join(&id).join("metadata.cbor"); - let metadata: BackupMetadata = IoFormat::Cbor.from_slice( - &tokio::fs::read(&metadata_path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, metadata_path.display().to_string()))?, - )?; - - let mut secrets = ctx.secret_store.acquire().await?; - let mut secrets_tx = secrets.begin().await?; - for (iface, key) in metadata.network_keys { - let k = key.0.as_slice(); - sqlx::query!( - "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - id.to_string(), - iface.to_string(), - k, - ) - .execute(secrets_tx.as_mut()).await?; - } - // DEPRECATED - for (iface, key) in metadata.tor_keys { - let k = key.0.as_slice(); - sqlx::query!( - "INSERT INTO tor (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - id.to_string(), - iface.to_string(), - k, - ) - .execute(secrets_tx.as_mut()).await?; - } - secrets_tx.commit().await?; - drop(secrets); - - let len = tokio::fs::metadata(&s9pk_path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, s9pk_path.display().to_string()))? - .len(); - let file = File::open(&s9pk_path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, s9pk_path.display().to_string()))?; - - let progress = InstallProgress::new(Some(len)); - let marketplace_url = metadata.marketplace_url; - - let progress = Arc::new(progress); - - ctx.db - .mutate(|db| { - db.as_package_data_mut().insert( - &id, - &PackageDataEntry::Restoring(PackageDataEntryRestoring { - install_progress: progress.clone(), - static_files: StaticFiles::local( - &id, - &manifest.version, - manifest.assets.icon_type(), - ), - manifest: manifest.clone(), - }), + let backup_dir = backup_guard.clone().package_backup(&id).await?; + let s9pk_path = backup_dir.path().join(&id).with_extension("s9pk"); + let task = ctx + .services + .install( + ctx.clone(), + || S9pk::open(s9pk_path, Some(&id)), + Some(backup_dir), + None, ) - }) - .await?; - Ok(( - progress.clone(), - async move { - download_install_s9pk(ctx, manifest, marketplace_url, progress, file, None).await?; - - guard.unmount().await?; + .await?; + tasks.insert(id, task); + } - Ok(()) - } - .boxed(), - )) + Ok(tasks) } diff --git a/core/startos/src/backup/target/cifs.rs b/core/startos/src/backup/target/cifs.rs index 3f3251535..71cbe267e 100644 --- a/core/startos/src/backup/target/cifs.rs +++ b/core/startos/src/backup/target/cifs.rs @@ -1,63 +1,118 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use clap::Parser; use color_eyre::eyre::eyre; -use futures::TryStreamExt; -use rpc_toolkit::command; +use imbl_value::InternedString; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use sqlx::{Executor, Postgres}; +use ts_rs::TS; use super::{BackupTarget, BackupTargetId}; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; +use crate::db::model::DatabaseModel; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::ReadOnly; -use crate::disk::mount::guard::TmpMountGuard; -use crate::disk::util::{recovery_info, EmbassyOsRecoveryInfo}; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; +use crate::disk::util::{recovery_info, StartOsRecoveryInfo}; use crate::prelude::*; -use crate::util::display_none; use crate::util::serde::KeyVal; +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct CifsTargets(pub BTreeMap); +impl CifsTargets { + pub fn new() -> Self { + Self(BTreeMap::new()) + } +} +impl Map for CifsTargets { + type Key = u32; + type Value = Cifs; + fn key_str(key: &Self::Key) -> Result, Error> { + Self::key_string(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::from_display(key)) + } +} + #[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct CifsBackupTarget { hostname: String, path: PathBuf, username: String, mountable: bool, - embassy_os: Option, + start_os: BTreeMap, } -#[command(subcommands(add, update, remove))] -pub fn cifs() -> Result<(), Error> { - Ok(()) +pub fn cifs() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add) + .no_display() + .with_about("Add a new backup target") + .with_call_remote::(), + ) + .subcommand( + "update", + from_fn_async(update) + .no_display() + .with_about("Update an existing backup target") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove) + .no_display() + .with_about("Remove an existing backup target") + .with_call_remote::(), + ) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct AddParams { + pub hostname: String, + pub path: PathBuf, + pub username: String, + pub password: Option, } -#[command(display(display_none))] pub async fn add( - #[context] ctx: RpcContext, - #[arg] hostname: String, - #[arg] path: PathBuf, - #[arg] username: String, - #[arg] password: Option, + ctx: RpcContext, + AddParams { + hostname, + path, + username, + password, + }: AddParams, ) -> Result, Error> { let cifs = Cifs { hostname, - path, + path: Path::new("/").join(path), username, password, }; let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?; - let embassy_os = recovery_info(&guard).await?; + let start_os = recovery_info(guard.path()).await?; guard.unmount().await?; - let path_string = Path::new("/").join(&cifs.path).display().to_string(); - let id: i32 = sqlx::query!( - "INSERT INTO cifs_shares (hostname, path, username, password) VALUES ($1, $2, $3, $4) RETURNING id", - cifs.hostname, - path_string, - cifs.username, - cifs.password, - ) - .fetch_one(&ctx.secret_store) - .await?.id; + let id = ctx + .db + .mutate(|db| { + let id = db + .as_private() + .as_cifs() + .keys()? + .into_iter() + .max() + .map_or(0, |a| a + 1); + db.as_private_mut().as_cifs_mut().insert(&id, &cifs)?; + Ok(id) + }) + .await?; Ok(KeyVal { key: BackupTargetId::Cifs { id }, value: BackupTarget::Cifs(CifsBackupTarget { @@ -65,19 +120,31 @@ pub async fn add( path: cifs.path, username: cifs.username, mountable: true, - embassy_os, + start_os, }), }) } -#[command(display(display_none))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct UpdateParams { + pub id: BackupTargetId, + pub hostname: String, + pub path: PathBuf, + pub username: String, + pub password: Option, +} + pub async fn update( - #[context] ctx: RpcContext, - #[arg] id: BackupTargetId, - #[arg] hostname: String, - #[arg] path: PathBuf, - #[arg] username: String, - #[arg] password: Option, + ctx: RpcContext, + UpdateParams { + id, + hostname, + path, + username, + password, + }: UpdateParams, ) -> Result, Error> { let id = if let BackupTargetId::Cifs { id } = id { id @@ -89,32 +156,27 @@ pub async fn update( }; let cifs = Cifs { hostname, - path, + path: Path::new("/").join(path), username, password, }; let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?; - let embassy_os = recovery_info(&guard).await?; + let start_os = recovery_info(guard.path()).await?; guard.unmount().await?; - let path_string = Path::new("/").join(&cifs.path).display().to_string(); - if sqlx::query!( - "UPDATE cifs_shares SET hostname = $1, path = $2, username = $3, password = $4 WHERE id = $5", - cifs.hostname, - path_string, - cifs.username, - cifs.password, - id, - ) - .execute(&ctx.secret_store) - .await? - .rows_affected() - == 0 - { - return Err(Error::new( - eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }), - ErrorKind::NotFound, - )); - }; + ctx.db + .mutate(|db| { + db.as_private_mut() + .as_cifs_mut() + .as_idx_mut(&id) + .ok_or_else(|| { + Error::new( + eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }), + ErrorKind::NotFound, + ) + })? + .ser(&cifs) + }) + .await?; Ok(KeyVal { key: BackupTargetId::Cifs { id }, value: BackupTarget::Cifs(CifsBackupTarget { @@ -122,13 +184,19 @@ pub async fn update( path: cifs.path, username: cifs.username, mountable: true, - embassy_os, + start_os, }), }) } -#[command(display(display_none))] -pub async fn remove(#[context] ctx: RpcContext, #[arg] id: BackupTargetId) -> Result<(), Error> { +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct RemoveParams { + pub id: BackupTargetId, +} + +pub async fn remove(ctx: RpcContext, RemoveParams { id }: RemoveParams) -> Result<(), Error> { let id = if let BackupTargetId::Cifs { id } = id { id } else { @@ -137,74 +205,46 @@ pub async fn remove(#[context] ctx: RpcContext, #[arg] id: BackupTargetId) -> Re ErrorKind::NotFound, )); }; - if sqlx::query!("DELETE FROM cifs_shares WHERE id = $1", id) - .execute(&ctx.secret_store) - .await? - .rows_affected() - == 0 - { - return Err(Error::new( - eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }), - ErrorKind::NotFound, - )); - }; + ctx.db + .mutate(|db| db.as_private_mut().as_cifs_mut().remove(&id)) + .await?; Ok(()) } -pub async fn load(secrets: &mut Ex, id: i32) -> Result -where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, -{ - let record = sqlx::query!( - "SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1", - id - ) - .fetch_one(secrets) - .await?; - - Ok(Cifs { - hostname: record.hostname, - path: PathBuf::from(record.path), - username: record.username, - password: record.password, - }) +pub fn load(db: &DatabaseModel, id: u32) -> Result { + db.as_private() + .as_cifs() + .as_idx(&id) + .ok_or_else(|| { + Error::new( + eyre!("Backup Target ID {} Not Found", id), + ErrorKind::NotFound, + ) + })? + .de() } -pub async fn list(secrets: &mut Ex) -> Result, Error> -where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, -{ - let mut records = - sqlx::query!("SELECT id, hostname, path, username, password FROM cifs_shares") - .fetch_many(secrets); - +pub async fn list(db: &DatabaseModel) -> Result, Error> { let mut cifs = Vec::new(); - while let Some(query_result) = records.try_next().await? { - if let Some(record) = query_result.right() { - let mount_info = Cifs { - hostname: record.hostname, - path: PathBuf::from(record.path), - username: record.username, - password: record.password, - }; - let embassy_os = async { - let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?; - let embassy_os = recovery_info(&guard).await?; - guard.unmount().await?; - Ok::<_, Error>(embassy_os) - } - .await; - cifs.push(( - record.id, - CifsBackupTarget { - hostname: mount_info.hostname, - path: mount_info.path, - username: mount_info.username, - mountable: embassy_os.is_ok(), - embassy_os: embassy_os.ok().and_then(|a| a), - }, - )); + for (id, model) in db.as_private().as_cifs().as_entries()? { + let mount_info = model.de()?; + let start_os = async { + let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?; + let start_os = recovery_info(guard.path()).await?; + guard.unmount().await?; + Ok::<_, Error>(start_os) } + .await; + cifs.push(( + id, + CifsBackupTarget { + hostname: mount_info.hostname, + path: mount_info.path, + username: mount_info.username, + mountable: start_os.is_ok(), + start_os: start_os.ok().unwrap_or_default(), + }, + )); } Ok(cifs) diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index 93e56c2d3..eb3fc29bc 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -1,39 +1,44 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use async_trait::async_trait; use chrono::{DateTime, Utc}; -use clap::ArgMatches; +use clap::builder::ValueParserFactory; +use clap::Parser; use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; -use rpc_toolkit::command; +use exver::Version; +use imbl_value::InternedString; +use models::{FromStrParser, PackageId}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use sqlx::{Executor, Postgres}; use tokio::sync::Mutex; use tracing::instrument; +use ts_rs::TS; use self::cifs::CifsBackupTarget; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; +use crate::db::model::DatabaseModel; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite}; -use crate::disk::mount::guard::TmpMountGuard; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::util::PartitionInfo; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::{deserialize_from_str, display_serializable, serialize_display}; -use crate::util::{display_none, Version}; +use crate::util::serde::{ + deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, WithIoFormat, +}; +use crate::util::VersionString; pub mod cifs; #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub enum BackupTarget { - #[serde(rename_all = "kebab-case")] + #[serde(rename_all = "camelCase")] Disk { vendor: Option, model: Option, @@ -43,21 +48,19 @@ pub enum BackupTarget { Cifs(CifsBackupTarget), } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, TS)] +#[ts(type = "string")] pub enum BackupTargetId { Disk { logicalname: PathBuf }, - Cifs { id: i32 }, + Cifs { id: u32 }, } impl BackupTargetId { - pub async fn load(self, secrets: &mut Ex) -> Result - where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, - { + pub fn load(self, db: &DatabaseModel) -> Result { Ok(match self { BackupTargetId::Disk { logicalname } => { BackupTargetFS::Disk(BlockDev::new(logicalname)) } - BackupTargetId::Cifs { id } => BackupTargetFS::Cifs(cifs::load(secrets, id).await?), + BackupTargetId::Cifs { id } => BackupTargetFS::Cifs(cifs::load(db, id)?), }) } } @@ -84,6 +87,12 @@ impl std::str::FromStr for BackupTargetId { } } } +impl ValueParserFactory for BackupTargetId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} impl<'de> Deserialize<'de> for BackupTargetId { fn deserialize(deserializer: D) -> Result where @@ -101,16 +110,15 @@ impl Serialize for BackupTargetId { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, TS)] #[serde(tag = "type")] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub enum BackupTargetFS { Disk(BlockDev), Cifs(Cifs), } -#[async_trait] impl FileSystem for BackupTargetFS { - async fn mount + Send + Sync>( + async fn mount + Send>( &self, mountpoint: P, mount_type: MountType, @@ -130,19 +138,51 @@ impl FileSystem for BackupTargetFS { } } -#[command(subcommands(cifs::cifs, list, info, mount, umount))] -pub fn target() -> Result<(), Error> { - Ok(()) +// #[command(subcommands(cifs::cifs, list, info, mount, umount))] +pub fn target() -> ParentHandler { + ParentHandler::new() + .subcommand( + "cifs", + cifs::cifs::().with_about("Add, remove, or update a backup target"), + ) + .subcommand( + "list", + from_fn_async(list) + .with_display_serializable() + .with_about("List existing backup targets") + .with_call_remote::(), + ) + .subcommand( + "info", + from_fn_async(info) + .with_display_serializable() + .with_custom_display_fn::(|params, info| { + Ok(display_backup_info(params.params, info)) + }) + .with_about("Display package backup information") + .with_call_remote::(), + ) + .subcommand( + "mount", + from_fn_async(mount) + .with_about("Mount backup target") + .with_call_remote::(), + ) + .subcommand( + "umount", + from_fn_async(umount) + .no_display() + .with_about("Unmount backup target") + .with_call_remote::(), + ) } -#[command(display(display_serializable))] -pub async fn list( - #[context] ctx: RpcContext, -) -> Result, Error> { - let mut sql_handle = ctx.secret_store.acquire().await?; +// #[command(display(display_serializable))] +pub async fn list(ctx: RpcContext) -> Result, Error> { + let peek = ctx.db.peek().await; let (disks_res, cifs) = tokio::try_join!( crate::disk::util::list(&ctx.os_partitions), - cifs::list(sql_handle.as_mut()), + cifs::list(&peek), )?; Ok(disks_res .into_iter() @@ -171,7 +211,7 @@ pub async fn list( } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct BackupInfo { pub version: Version, pub timestamp: Option>, @@ -179,19 +219,19 @@ pub struct BackupInfo { } #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { - pub title: String, - pub version: Version, + pub title: InternedString, + pub version: VersionString, pub os_version: Version, pub timestamp: DateTime, } -fn display_backup_info(info: BackupInfo, matches: &ArgMatches) { +fn display_backup_info(params: WithIoFormat, info: BackupInfo) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(info, matches); + if let Some(format) = params.format { + return display_serializable(format, info); } let mut table = Table::new(); @@ -202,9 +242,9 @@ fn display_backup_info(info: BackupInfo, matches: &ArgMatches) { "TIMESTAMP", ]); table.add_row(row![ - "EMBASSY OS", - info.version.as_str(), - info.version.as_str(), + "StartOS", + &info.version.to_string(), + &info.version.to_string(), &if let Some(ts) = &info.timestamp { ts.to_string() } else { @@ -215,7 +255,7 @@ fn display_backup_info(info: BackupInfo, matches: &ArgMatches) { let row = row![ &*id, info.version.as_str(), - info.os_version.as_str(), + &info.os_version.to_string(), &info.timestamp.to_string(), ]; table.add_row(row); @@ -223,21 +263,27 @@ fn display_backup_info(info: BackupInfo, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_backup_info))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct InfoParams { + target_id: BackupTargetId, + server_id: String, + password: String, +} + #[instrument(skip(ctx, password))] pub async fn info( - #[context] ctx: RpcContext, - #[arg(rename = "target-id")] target_id: BackupTargetId, - #[arg] password: String, + ctx: RpcContext, + InfoParams { + target_id, + server_id, + password, + }: InfoParams, ) -> Result { let guard = BackupMountGuard::mount( - TmpMountGuard::mount( - &target_id - .load(ctx.secret_store.acquire().await?.as_mut()) - .await?, - ReadWrite, - ) - .await?, + TmpMountGuard::mount(&target_id.load(&ctx.db.peek().await)?, ReadWrite).await?, + &server_id, &password, ) .await?; @@ -254,45 +300,54 @@ lazy_static::lazy_static! { Mutex::new(BTreeMap::new()); } -#[command] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct MountParams { + target_id: BackupTargetId, + server_id: String, + password: String, +} + #[instrument(skip_all)] pub async fn mount( - #[context] ctx: RpcContext, - #[arg(rename = "target-id")] target_id: BackupTargetId, - #[arg] password: String, + ctx: RpcContext, + MountParams { + target_id, + server_id, + password, + }: MountParams, ) -> Result { let mut mounts = USER_MOUNTS.lock().await; if let Some(existing) = mounts.get(&target_id) { - return Ok(existing.as_ref().display().to_string()); + return Ok(existing.path().display().to_string()); } let guard = BackupMountGuard::mount( - TmpMountGuard::mount( - &target_id - .clone() - .load(ctx.secret_store.acquire().await?.as_mut()) - .await?, - ReadWrite, - ) - .await?, + TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await?, + &server_id, &password, ) .await?; - let res = guard.as_ref().display().to_string(); + let res = guard.path().display().to_string(); mounts.insert(target_id, guard); Ok(res) } -#[command(display(display_none))] + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct UmountParams { + target_id: Option, +} + #[instrument(skip_all)] -pub async fn umount( - #[context] _ctx: RpcContext, - #[arg(rename = "target-id")] target_id: Option, -) -> Result<(), Error> { - let mut mounts = USER_MOUNTS.lock().await; +pub async fn umount(_: RpcContext, UmountParams { target_id }: UmountParams) -> Result<(), Error> { + let mut mounts = USER_MOUNTS.lock().await; // TODO: move to context if let Some(target_id) = target_id { if let Some(existing) = mounts.remove(&target_id) { existing.unmount().await?; diff --git a/core/startos/src/bins/avahi_alias.rs b/core/startos/src/bins/avahi_alias.rs deleted file mode 100644 index 3c4a4fe7e..000000000 --- a/core/startos/src/bins/avahi_alias.rs +++ /dev/null @@ -1,163 +0,0 @@ -use avahi_sys::{ - self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit, - avahi_strerror, AvahiClient, -}; - -fn log_str_error(action: &str, e: i32) { - unsafe { - let e_str = avahi_strerror(e); - eprintln!( - "Could not {}: {:?}", - action, - std::ffi::CStr::from_ptr(e_str) - ); - } -} - -pub fn main() { - let aliases: Vec<_> = std::env::args().skip(1).collect(); - unsafe { - let simple_poll = avahi_sys::avahi_simple_poll_new(); - let poll = avahi_sys::avahi_simple_poll_get(simple_poll); - let mut box_err = Box::pin(0 as i32); - let err_c: *mut i32 = box_err.as_mut().get_mut(); - let avahi_client = avahi_sys::avahi_client_new( - poll, - avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, - Some(client_callback), - std::ptr::null_mut(), - err_c, - ); - if avahi_client == std::ptr::null_mut::() { - log_str_error("create Avahi client", *box_err); - panic!("Failed to create Avahi Client"); - } - let group = avahi_sys::avahi_entry_group_new( - avahi_client, - Some(entry_group_callback), - std::ptr::null_mut(), - ); - if group == std::ptr::null_mut() { - log_str_error("create Avahi entry group", avahi_client_errno(avahi_client)); - panic!("Failed to create Avahi Entry Group"); - } - let mut hostname_buf = vec![0]; - let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client); - hostname_buf.extend_from_slice(std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul()); - let buflen = hostname_buf.len(); - debug_assert!(hostname_buf.ends_with(b".local\0")); - debug_assert!(!hostname_buf[..(buflen - 7)].contains(&b'.')); - // assume fixed length prefix on hostname due to local address - hostname_buf[0] = (buflen - 8) as u8; // set the prefix length to len - 8 (leading byte, .local, nul) for the main address - hostname_buf[buflen - 7] = 5; // set the prefix length to 5 for "local" - let mut res; - let http_tcp_cstr = - std::ffi::CString::new("_http._tcp").expect("Could not cast _http._tcp to c string"); - res = avahi_entry_group_add_service( - group, - avahi_sys::AVAHI_IF_UNSPEC, - avahi_sys::AVAHI_PROTO_UNSPEC, - avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST, - hostname_raw, - http_tcp_cstr.as_ptr(), - std::ptr::null(), - std::ptr::null(), - 443, - // below is a secret final argument that the type signature of this function does not tell you that it - // needs. This is because the C lib function takes a variable number of final arguments indicating the - // desired TXT records to add to this service entry. The way it decides when to stop taking arguments - // from the stack and dereferencing them is when it finds a null pointer...because fuck you, that's why. - // The consequence of this is that forgetting this last argument will cause segfaults or other undefined - // behavior. Welcome back to the stone age motherfucker. - std::ptr::null::(), - ); - if res < avahi_sys::AVAHI_OK { - log_str_error("add service to Avahi entry group", res); - panic!("Failed to load Avahi services"); - } - eprintln!("Published {:?}", std::ffi::CStr::from_ptr(hostname_raw)); - for alias in aliases { - let lan_address = alias + ".local"; - let lan_address_ptr = std::ffi::CString::new(lan_address) - .expect("Could not cast lan address to c string"); - res = avahi_sys::avahi_entry_group_add_record( - group, - avahi_sys::AVAHI_IF_UNSPEC, - avahi_sys::AVAHI_PROTO_UNSPEC, - avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST - | avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE, - lan_address_ptr.as_ptr(), - avahi_sys::AVAHI_DNS_CLASS_IN as u16, - avahi_sys::AVAHI_DNS_TYPE_CNAME as u16, - avahi_sys::AVAHI_DEFAULT_TTL, - hostname_buf.as_ptr().cast(), - hostname_buf.len(), - ); - if res < avahi_sys::AVAHI_OK { - log_str_error("add CNAME record to Avahi entry group", res); - panic!("Failed to load Avahi services"); - } - eprintln!("Published {:?}", lan_address_ptr); - } - let commit_err = avahi_entry_group_commit(group); - if commit_err < avahi_sys::AVAHI_OK { - log_str_error("reset Avahi entry group", commit_err); - panic!("Failed to load Avahi services: reset"); - } - } - std::thread::park() -} - -unsafe extern "C" fn entry_group_callback( - _group: *mut avahi_sys::AvahiEntryGroup, - state: avahi_sys::AvahiEntryGroupState, - _userdata: *mut core::ffi::c_void, -) { - match state { - avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_FAILURE => { - eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_FAILURE"); - } - avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_COLLISION => { - eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_COLLISION"); - } - avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_UNCOMMITED => { - eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_UNCOMMITED"); - } - avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_ESTABLISHED => { - eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_ESTABLISHED"); - } - avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_REGISTERING => { - eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_REGISTERING"); - } - other => { - eprintln!("AvahiCallback: EntryGroupState = {}", other); - } - } -} - -unsafe extern "C" fn client_callback( - _group: *mut avahi_sys::AvahiClient, - state: avahi_sys::AvahiClientState, - _userdata: *mut core::ffi::c_void, -) { - match state { - avahi_sys::AvahiClientState_AVAHI_CLIENT_FAILURE => { - eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_FAILURE"); - } - avahi_sys::AvahiClientState_AVAHI_CLIENT_S_RUNNING => { - eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_RUNNING"); - } - avahi_sys::AvahiClientState_AVAHI_CLIENT_CONNECTING => { - eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_CONNECTING"); - } - avahi_sys::AvahiClientState_AVAHI_CLIENT_S_COLLISION => { - eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_COLLISION"); - } - avahi_sys::AvahiClientState_AVAHI_CLIENT_S_REGISTERING => { - eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_REGISTERING"); - } - other => { - eprintln!("AvahiCallback: ClientState = {}", other); - } - } -} diff --git a/core/startos/src/bins/container_cli.rs b/core/startos/src/bins/container_cli.rs new file mode 100644 index 000000000..118133f55 --- /dev/null +++ b/core/startos/src/bins/container_cli.rs @@ -0,0 +1,38 @@ +use std::ffi::OsString; + +use rpc_toolkit::CliApp; +use serde_json::Value; + +use crate::service::cli::{ContainerCliContext, ContainerClientConfig}; +use crate::util::logger::LOGGER; +use crate::version::{Current, VersionT}; + +lazy_static::lazy_static! { + static ref VERSION_STRING: String = Current::default().semver().to_string(); +} + +pub fn main(args: impl IntoIterator) { + LOGGER.enable(); + if let Err(e) = CliApp::new( + |cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)), + crate::service::effects::handler(), + ) + .run(args) + { + match e.data { + Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), + Some(Value::Object(o)) => { + if let Some(Value::String(s)) = o.get("details") { + eprintln!("{}: {}", e.message, s); + if let Some(Value::String(s)) = o.get("debug") { + tracing::debug!("{}", s) + } + } + } + Some(a) => eprintln!("{}: {}", e.message, a), + None => eprintln!("{}", e.message), + } + + std::process::exit(e.code); + } +} diff --git a/core/startos/src/bins/mod.rs b/core/startos/src/bins/mod.rs index f9c88cae9..6ffecfce9 100644 --- a/core/startos/src/bins/mod.rs +++ b/core/startos/src/bins/mod.rs @@ -1,49 +1,68 @@ +use std::collections::VecDeque; +use std::ffi::OsString; use std::path::Path; -#[cfg(feature = "avahi-alias")] -pub mod avahi_alias; +#[cfg(feature = "container-runtime")] +pub mod container_cli; pub mod deprecated; +#[cfg(feature = "registry")] +pub mod registry; #[cfg(feature = "cli")] pub mod start_cli; -#[cfg(feature = "js-engine")] -pub mod start_deno; #[cfg(feature = "daemon")] pub mod start_init; -#[cfg(feature = "sdk")] -pub mod start_sdk; #[cfg(feature = "daemon")] pub mod startd; -fn select_executable(name: &str) -> Option { +fn select_executable(name: &str) -> Option)> { match name { - #[cfg(feature = "avahi-alias")] - "avahi-alias" => Some(avahi_alias::main), - #[cfg(feature = "js-engine")] - "start-deno" => Some(start_deno::main), #[cfg(feature = "cli")] "start-cli" => Some(start_cli::main), - #[cfg(feature = "sdk")] - "start-sdk" => Some(start_sdk::main), + #[cfg(feature = "container-runtime")] + "start-cli" => Some(container_cli::main), #[cfg(feature = "daemon")] "startd" => Some(startd::main), - "embassy-cli" => Some(|| deprecated::renamed("embassy-cli", "start-cli")), - "embassy-sdk" => Some(|| deprecated::renamed("embassy-sdk", "start-sdk")), - "embassyd" => Some(|| deprecated::renamed("embassyd", "startd")), - "embassy-init" => Some(|| deprecated::removed("embassy-init")), + #[cfg(feature = "registry")] + "registry" => Some(registry::main), + "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), + "embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), + "embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), + "embassy-init" => Some(|_| deprecated::removed("embassy-init")), + "contents" => Some(|_| { + #[cfg(feature = "cli")] + println!("start-cli"); + #[cfg(feature = "container-runtime")] + println!("start-cli (container)"); + #[cfg(feature = "daemon")] + println!("startd"); + #[cfg(feature = "registry")] + println!("registry"); + }), _ => None, } } pub fn startbox() { - let args = std::env::args().take(2).collect::>(); - let executable = args - .get(0) - .and_then(|s| Path::new(&*s).file_name()) - .and_then(|s| s.to_str()); - if let Some(x) = executable.and_then(|s| select_executable(&s)) { - x() - } else { - eprintln!("unknown executable: {}", executable.unwrap_or("N/A")); - std::process::exit(1); + let mut args = std::env::args_os().collect::>(); + for _ in 0..2 { + if let Some(s) = args.pop_front() { + if let Some(x) = Path::new(&*s) + .file_name() + .and_then(|s| s.to_str()) + .and_then(|s| select_executable(&s)) + { + args.push_front(s); + return x(args); + } + } } + let args = std::env::args().collect::>(); + eprintln!( + "unknown executable: {}", + args.get(1) + .or_else(|| args.get(0)) + .map(|s| s.as_str()) + .unwrap_or("N/A") + ); + std::process::exit(1); } diff --git a/core/startos/src/bins/registry.rs b/core/startos/src/bins/registry.rs new file mode 100644 index 000000000..a71b737af --- /dev/null +++ b/core/startos/src/bins/registry.rs @@ -0,0 +1,87 @@ +use std::ffi::OsString; + +use clap::Parser; +use futures::FutureExt; +use tokio::signal::unix::signal; +use tracing::instrument; + +use crate::net::web_server::{Acceptor, WebServer}; +use crate::prelude::*; +use crate::registry::context::{RegistryConfig, RegistryContext}; +use crate::util::logger::LOGGER; + +#[instrument(skip_all)] +async fn inner_main(config: &RegistryConfig) -> Result<(), Error> { + let server = async { + let ctx = RegistryContext::init(config).await?; + let mut server = WebServer::new(Acceptor::bind([ctx.listen]).await?); + server.serve_registry(ctx.clone()); + + let mut shutdown_recv = ctx.shutdown.subscribe(); + + let sig_handler_ctx = ctx; + let sig_handler = tokio::spawn(async move { + use tokio::signal::unix::SignalKind; + futures::future::select_all( + [ + SignalKind::interrupt(), + SignalKind::quit(), + SignalKind::terminate(), + ] + .iter() + .map(|s| { + async move { + signal(*s) + .unwrap_or_else(|_| panic!("register {:?} handler", s)) + .recv() + .await + } + .boxed() + }), + ) + .await; + sig_handler_ctx + .shutdown + .send(()) + .map_err(|_| ()) + .expect("send shutdown signal"); + }); + + shutdown_recv + .recv() + .await + .with_kind(crate::ErrorKind::Unknown)?; + + sig_handler.abort(); + + Ok::<_, Error>(server) + } + .await?; + server.shutdown().await; + + Ok(()) +} + +pub fn main(args: impl IntoIterator) { + LOGGER.enable(); + + let config = RegistryConfig::parse_from(args).load().unwrap(); + + let res = { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to initialize runtime"); + rt.block_on(inner_main(&config)) + }; + + match res { + Ok(()) => (), + Err(e) => { + eprintln!("{}", e.source); + tracing::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/core/startos/src/bins/start_cli.rs b/core/startos/src/bins/start_cli.rs index 3ef64096e..bda5e00d3 100644 --- a/core/startos/src/bins/start_cli.rs +++ b/core/startos/src/bins/start_cli.rs @@ -1,62 +1,40 @@ -use clap::Arg; -use rpc_toolkit::run_cli; -use rpc_toolkit::yajrc::RpcError; +use std::ffi::OsString; + +use rpc_toolkit::CliApp; use serde_json::Value; +use crate::context::config::ClientConfig; use crate::context::CliContext; -use crate::util::logger::EmbassyLogger; +use crate::util::logger::LOGGER; use crate::version::{Current, VersionT}; -use crate::Error; lazy_static::lazy_static! { - static ref VERSION_STRING: String = Current::new().semver().to_string(); + static ref VERSION_STRING: String = Current::default().semver().to_string(); } -fn inner_main() -> Result<(), Error> { - run_cli!({ - command: crate::main_api, - app: app => app - .name("StartOS CLI") - .version(&**VERSION_STRING) - .arg( - clap::Arg::with_name("config") - .short('c') - .long("config") - .takes_value(true), - ) - .arg(Arg::with_name("host").long("host").short('h').takes_value(true)) - .arg(Arg::with_name("proxy").long("proxy").short('p').takes_value(true)), - context: matches => { - EmbassyLogger::init(); - CliContext::init(matches)? - }, - exit: |e: RpcError| { - match e.data { - Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), - Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("details") { +pub fn main(args: impl IntoIterator) { + LOGGER.enable(); + + if let Err(e) = CliApp::new( + |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), + crate::expanded_api(), + ) + .run(args) + { + match e.data { + Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), + Some(Value::Object(o)) => { + if let Some(Value::String(s)) = o.get("details") { eprintln!("{}: {}", e.message, s); if let Some(Value::String(s)) = o.get("debug") { tracing::debug!("{}", s) } } - Some(a) => eprintln!("{}: {}", e.message, a), - None => eprintln!("{}", e.message), } - - std::process::exit(e.code); + Some(a) => eprintln!("{}: {}", e.message, a), + None => eprintln!("{}", e.message), } - }); - Ok(()) -} -pub fn main() { - match inner_main() { - Ok(_) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } + std::process::exit(e.code); } } diff --git a/core/startos/src/bins/start_deno.rs b/core/startos/src/bins/start_deno.rs deleted file mode 100644 index 8f5a1451a..000000000 --- a/core/startos/src/bins/start_deno.rs +++ /dev/null @@ -1,140 +0,0 @@ -use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, run_cli, Context}; -use serde_json::Value; - -use crate::procedure::js_scripts::ExecuteArgs; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat}; -use crate::version::{Current, VersionT}; -use crate::Error; - -lazy_static::lazy_static! { - static ref VERSION_STRING: String = Current::new().semver().to_string(); -} - -struct DenoContext; -impl Context for DenoContext {} - -#[command(subcommands(execute, sandbox))] -fn deno_api() -> Result<(), Error> { - Ok(()) -} - -#[command(cli_only, display(display_serializable))] -async fn execute( - #[arg(stdin, parse(parse_stdin_deserializable))] arg: ExecuteArgs, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result, Error> { - let ExecuteArgs { - procedure, - directory, - pkg_id, - pkg_version, - name, - volumes, - input, - } = arg; - PackageLogger::init(&pkg_id); - procedure - .execute_impl(&directory, &pkg_id, &pkg_version, name, &volumes, input) - .await -} -#[command(cli_only, display(display_serializable))] -async fn sandbox( - #[arg(stdin, parse(parse_stdin_deserializable))] arg: ExecuteArgs, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result, Error> { - let ExecuteArgs { - procedure, - directory, - pkg_id, - pkg_version, - name, - volumes, - input, - } = arg; - PackageLogger::init(&pkg_id); - procedure - .sandboxed_impl(&directory, &pkg_id, &pkg_version, &volumes, input, name) - .await -} - -use tracing::Subscriber; -use tracing_subscriber::util::SubscriberInitExt; - -#[derive(Clone)] -struct PackageLogger {} - -impl PackageLogger { - fn base_subscriber(id: &PackageId) -> impl Subscriber { - use tracing_error::ErrorLayer; - use tracing_subscriber::prelude::*; - use tracing_subscriber::{fmt, EnvFilter}; - - let filter_layer = EnvFilter::default().add_directive( - format!("{}=warn", std::module_path!().split("::").next().unwrap()) - .parse() - .unwrap(), - ); - let fmt_layer = fmt::layer().with_writer(std::io::stderr).with_target(true); - let journald_layer = tracing_journald::layer() - .unwrap() - .with_syslog_identifier(format!("{id}.embassy")); - - let sub = tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(journald_layer) - .with(ErrorLayer::default()); - - sub - } - pub fn init(id: &PackageId) -> Self { - Self::base_subscriber(id).init(); - color_eyre::install().unwrap_or_else(|_| tracing::warn!("tracing too many times")); - - Self {} - } -} - -fn inner_main() -> Result<(), Error> { - run_cli!({ - command: deno_api, - app: app => app - .name("StartOS Deno Executor") - .version(&**VERSION_STRING), - context: _m => DenoContext, - exit: |e: RpcError| { - match e.data { - Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), - Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("details") { - eprintln!("{}: {}", e.message, s); - if let Some(Value::String(s)) = o.get("debug") { - tracing::debug!("{}", s) - } - } - Some(a) => eprintln!("{}: {}", e.message, a), - None => eprintln!("{}", e.message), - } - - std::process::exit(e.code); - } - }); - Ok(()) -} - -pub fn main() { - match inner_main() { - Ok(_) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } - } -} diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index 1cb070851..fdd0e075d 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -1,47 +1,57 @@ -use std::net::{Ipv6Addr, SocketAddr}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; -use std::time::Duration; -use helpers::NonDetachingJoinHandle; use tokio::process::Command; use tracing::instrument; -use crate::context::rpc::RpcContextConfig; -use crate::context::{DiagnosticContext, InstallContext, SetupContext}; -use crate::disk::fsck::{RepairStrategy, RequiresReboot}; +use crate::context::config::ServerConfig; +use crate::context::rpc::InitRpcContextPhases; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; +use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::REPAIR_DISK_PATH; -use crate::firmware::update_firmware; -use crate::init::STANDBY_MODE_PATH; -use crate::net::web_server::WebServer; +use crate::firmware::{check_for_firmware_update, update_firmware}; +use crate::init::{InitPhases, STANDBY_MODE_PATH}; +use crate::net::web_server::{UpgradableListener, WebServer}; +use crate::prelude::*; +use crate::progress::FullProgressTracker; use crate::shutdown::Shutdown; -use crate::sound::{BEP, CHIME}; use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt, PLATFORM}; +use crate::{DATA_DIR, PLATFORM}; #[instrument(skip_all)] -async fn setup_or_init(cfg_path: Option) -> Result, Error> { - let song = NonDetachingJoinHandle::from(tokio::spawn(async { - loop { - BEP.play().await.unwrap(); - BEP.play().await.unwrap(); - tokio::time::sleep(Duration::from_secs(30)).await; - } - })); +async fn setup_or_init( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { + if let Some(firmware) = check_for_firmware_update() + .await + .map_err(|e| { + tracing::warn!("Error checking for firmware update: {e}"); + tracing::debug!("{e:?}"); + }) + .ok() + .and_then(|a| a) + { + let init_ctx = InitContext::init(config).await?; + let handle = &init_ctx.progress; + let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10)); + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); - match update_firmware().await { - Ok(RequiresReboot(true)) => { - return Ok(Some(Shutdown { - export_args: None, - restart: true, - })) - } - Err(e) => { + server.serve_init(init_ctx); + + update_phase.start(); + if let Err(e) = update_firmware(firmware).await { tracing::warn!("Error performing firmware update: {e}"); tracing::debug!("{e:?}"); + } else { + update_phase.complete(); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: None, + restart: true, + })); } - _ => (), } Command::new("ln") @@ -82,17 +92,9 @@ async fn setup_or_init(cfg_path: Option) -> Result, Er .invoke(crate::ErrorKind::OpenSsh) .await?; - let ctx = InstallContext::init(cfg_path).await?; + let ctx = InstallContext::init().await?; - let server = WebServer::install( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - ) - .await?; - - drop(song); - tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this - CHIME.play().await?; + server.serve_install(ctx.clone()); ctx.shutdown .subscribe() @@ -100,34 +102,22 @@ async fn setup_or_init(cfg_path: Option) -> Result, Er .await .expect("context dropped"); - server.shutdown().await; + return Ok(Err(Shutdown { + export_args: None, + restart: true, + })); + } - Command::new("reboot") - .invoke(crate::ErrorKind::Unknown) - .await?; - } else if tokio::fs::metadata("/media/embassy/config/disk.guid") + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_err() { - let ctx = SetupContext::init(cfg_path).await?; - - let server = WebServer::setup( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - ) - .await?; - - drop(song); - tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this - CHIME.play().await?; + let ctx = SetupContext::init(server, config)?; - ctx.shutdown - .subscribe() - .recv() - .await - .expect("context dropped"); + server.serve_setup(ctx.clone()); - server.shutdown().await; + let mut shutdown = ctx.shutdown.subscribe(); + shutdown.recv().await.expect("context dropped"); tokio::task::yield_now().await; if let Err(e) = Command::new("killall") @@ -138,65 +128,93 @@ async fn setup_or_init(cfg_path: Option) -> Result, Er tracing::error!("Failed to kill kiosk: {}", e); tracing::debug!("{:?}", e); } + + Ok(Ok(match ctx.result.get() { + Some(Ok((_, rpc_ctx))) => (rpc_ctx.clone(), ctx.progress.clone()), + Some(Err(e)) => return Err(e.clone_output()), + None => { + return Err(Error::new( + eyre!("Setup mode exited before setup completed"), + ErrorKind::Unknown, + )) + } + })) } else { - let cfg = RpcContextConfig::load(cfg_path).await?; - let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy - .await?; - let guid = guid_string.trim(); - let requires_reboot = crate::disk::main::import( - guid, - cfg.datadir(), - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - RepairStrategy::Aggressive - } else { - RepairStrategy::Preen - }, - if guid.ends_with("_UNENC") { - None - } else { - Some(DEFAULT_PASSWORD) - }, - ) - .await?; - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - tokio::fs::remove_file(REPAIR_DISK_PATH) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; - } - if requires_reboot.0 { - crate::disk::main::export(guid, cfg.datadir()).await?; - Command::new("reboot") - .invoke(crate::ErrorKind::Unknown) - .await?; - } - tracing::info!("Loaded Disk"); - crate::init::init(&cfg).await?; - drop(song); - } + let init_ctx = InitContext::init(config).await?; + let handle = init_ctx.progress.clone(); + let err_channel = init_ctx.error.clone(); - Ok(None) -} + let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); + let init_phases = InitPhases::new(&handle); + let rpc_ctx_phases = InitRpcContextPhases::new(&handle); + + server.serve_init(init_ctx); -async fn run_script_if_exists>(path: P) { - let script = path.as_ref(); - if script.exists() { - match Command::new("/bin/bash").arg(script).spawn() { - Ok(mut c) => { - if let Err(e) = c.wait().await { - tracing::error!("Error Running {}: {}", script.display(), e); - tracing::debug!("{:?}", e); - } + async { + disk_phase.start(); + let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + .await?; + let disk_guid = Arc::new(String::from(guid_string.trim())); + let requires_reboot = crate::disk::main::import( + &**disk_guid, + DATA_DIR, + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + RepairStrategy::Aggressive + } else { + RepairStrategy::Preen + }, + if disk_guid.ends_with("_UNENC") { + None + } else { + Some(DEFAULT_PASSWORD) + }, + ) + .await?; + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; } - Err(e) => { - tracing::error!("Error Running {}: {}", script.display(), e); - tracing::debug!("{:?}", e); + disk_phase.complete(); + tracing::info!("Loaded Disk"); + + if requires_reboot.0 { + tracing::info!("Rebooting..."); + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: Some((disk_guid, Path::new(DATA_DIR).to_owned())), + restart: true, + })); } + + let init_result = + crate::init::init(&server.acceptor_setter(), config, init_phases).await?; + + let rpc_ctx = RpcContext::init( + &server.acceptor_setter(), + config, + disk_guid, + Some(init_result), + rpc_ctx_phases, + ) + .await?; + + Ok::<_, Error>(Ok((rpc_ctx, handle))) } + .await + .map_err(|e| { + err_channel.send_replace(Some(e.clone_output())); + e + }) } } #[instrument(skip_all)] -async fn inner_main(cfg_path: Option) -> Result, Error> { +pub async fn main( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { if &*PLATFORM == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() { tokio::fs::remove_file(STANDBY_MODE_PATH).await?; Command::new("sync").invoke(ErrorKind::Filesystem).await?; @@ -204,25 +222,20 @@ async fn inner_main(cfg_path: Option) -> Result, Error futures::future::pending::<()>().await; } - crate::sound::BEP.play().await?; - - run_script_if_exists("/media/embassy/config/preinit.sh").await; - - let res = match setup_or_init(cfg_path.clone()).await { + let res = match setup_or_init(server, config).await { Err(e) => { async move { - tracing::error!("{}", e.source); - tracing::debug!("{}", e.source); - crate::sound::BEETHOVEN.play().await?; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); let ctx = DiagnosticContext::init( - cfg_path, - if tokio::fs::metadata("/media/embassy/config/disk.guid") + config, + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_ok() { Some(Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), @@ -231,58 +244,18 @@ async fn inner_main(cfg_path: Option) -> Result, Error None }, e, - ) - .await?; + )?; - let server = WebServer::diagnostic( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - ) - .await?; + server.serve_diagnostic(ctx.clone()); let shutdown = ctx.shutdown.subscribe().recv().await.unwrap(); - server.shutdown().await; - - Ok(shutdown) + Ok(Err(shutdown)) } .await } Ok(s) => Ok(s), }; - run_script_if_exists("/media/embassy/config/postinit.sh").await; - res } - -pub fn main() { - let matches = clap::App::new("start-init") - .arg( - clap::Arg::with_name("config") - .short('c') - .long("config") - .takes_value(true), - ) - .get_matches(); - - let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned()); - let res = { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("failed to initialize runtime"); - rt.block_on(inner_main(cfg_path)) - }; - - match res { - Ok(Some(shutdown)) => shutdown.execute(), - Ok(None) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } - } -} diff --git a/core/startos/src/bins/start_sdk.rs b/core/startos/src/bins/start_sdk.rs deleted file mode 100644 index 10219c485..000000000 --- a/core/startos/src/bins/start_sdk.rs +++ /dev/null @@ -1,61 +0,0 @@ -use rpc_toolkit::run_cli; -use rpc_toolkit::yajrc::RpcError; -use serde_json::Value; - -use crate::context::SdkContext; -use crate::util::logger::EmbassyLogger; -use crate::version::{Current, VersionT}; -use crate::Error; - -lazy_static::lazy_static! { - static ref VERSION_STRING: String = Current::new().semver().to_string(); -} - -fn inner_main() -> Result<(), Error> { - run_cli!({ - command: crate::portable_api, - app: app => app - .name("StartOS SDK") - .version(&**VERSION_STRING) - .arg( - clap::Arg::with_name("config") - .short('c') - .long("config") - .takes_value(true), - ), - context: matches => { - if let Err(_) = std::env::var("RUST_LOG") { - std::env::set_var("RUST_LOG", "embassy=warn,js_engine=warn"); - } - EmbassyLogger::init(); - SdkContext::init(matches)? - }, - exit: |e: RpcError| { - match e.data { - Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), - Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("details") { - eprintln!("{}: {}", e.message, s); - if let Some(Value::String(s)) = o.get("debug") { - tracing::debug!("{}", s) - } - } - Some(a) => eprintln!("{}: {}", e.message, a), - None => eprintln!("{}", e.message), - } - std::process::exit(e.code); - } - }); - Ok(()) -} - -pub fn main() { - match inner_main() { - Ok(_) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } - } -} diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index a773dd99a..b7574b7c7 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -1,38 +1,77 @@ -use std::net::{Ipv6Addr, SocketAddr}; -use std::path::{Path, PathBuf}; +use std::cmp::max; +use std::ffi::OsString; +use std::net::IpAddr; use std::sync::Arc; +use clap::Parser; use color_eyre::eyre::eyre; use futures::{FutureExt, TryFutureExt}; use tokio::signal::unix::signal; use tracing::instrument; -use crate::context::{DiagnosticContext, RpcContext}; -use crate::net::web_server::WebServer; +use crate::context::config::ServerConfig; +use crate::context::rpc::InitRpcContextPhases; +use crate::context::{DiagnosticContext, InitContext, RpcContext}; +use crate::net::network_interface::SelfContainedNetworkInterfaceListener; +use crate::net::utils::ipv6_is_local; +use crate::net::web_server::{Acceptor, UpgradableListener, WebServer}; use crate::shutdown::Shutdown; use crate::system::launch_metrics_task; -use crate::util::logger::EmbassyLogger; +use crate::util::io::append_file; +use crate::util::logger::LOGGER; use crate::{Error, ErrorKind, ResultExt}; #[instrument(skip_all)] -async fn inner_main(cfg_path: Option) -> Result, Error> { - let (rpc_ctx, server, shutdown) = async { - let rpc_ctx = RpcContext::init( - cfg_path, +async fn inner_main( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { + let rpc_ctx = if !tokio::fs::metadata("/run/startos/initialized") + .await + .is_ok() + { + LOGGER.set_logfile(Some( + append_file("/run/startos/init.log").await?.into_std().await, + )); + let (ctx, handle) = match super::start_init::main(server, &config).await? { + Err(s) => return Ok(Some(s)), + Ok(ctx) => ctx, + }; + tokio::fs::write("/run/startos/initialized", "").await?; + + server.serve_main(ctx.clone()); + LOGGER.set_logfile(None); + handle.complete(); + + ctx + } else { + let init_ctx = InitContext::init(config).await?; + let handle = init_ctx.progress.clone(); + let rpc_ctx_phases = InitRpcContextPhases::new(&handle); + server.serve_init(init_ctx); + + let ctx = RpcContext::init( + &server.acceptor_setter(), + config, Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), ), + None, + rpc_ctx_phases, ) .await?; + + server.serve_main(ctx.clone()); + handle.complete(); + + ctx + }; + + let (rpc_ctx, shutdown) = async { crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?; - let server = WebServer::main( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - rpc_ctx.clone(), - ) - .await?; let mut shutdown_recv = rpc_ctx.shutdown.subscribe(); @@ -72,8 +111,6 @@ async fn inner_main(cfg_path: Option) -> Result, Error .await }); - crate::sound::CHIME.play().await?; - metrics_task .map_err(|e| { Error::new( @@ -91,10 +128,9 @@ async fn inner_main(cfg_path: Option) -> Result, Error sig_handler.abort(); - Ok::<_, Error>((rpc_ctx, server, shutdown)) + Ok::<_, Error>((rpc_ctx, shutdown)) } .await?; - server.shutdown().await; rpc_ctx.shutdown().await?; tracing::info!("RPC Context is dropped"); @@ -102,46 +138,39 @@ async fn inner_main(cfg_path: Option) -> Result, Error Ok(shutdown) } -pub fn main() { - EmbassyLogger::init(); - - if !Path::new("/run/embassy/initialized").exists() { - super::start_init::main(); - std::fs::write("/run/embassy/initialized", "").unwrap(); - } - - let matches = clap::App::new("startd") - .arg( - clap::Arg::with_name("config") - .short('c') - .long("config") - .takes_value(true), - ) - .get_matches(); +pub fn main(args: impl IntoIterator) { + LOGGER.enable(); - let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned()); + let config = ServerConfig::parse_from(args).load().unwrap(); let res = { let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(max(4, num_cpus::get())) .enable_all() .build() .expect("failed to initialize runtime"); rt.block_on(async { - match inner_main(cfg_path.clone()).await { - Ok(a) => Ok(a), + let addrs = crate::net::utils::all_socket_addrs_for(80).await?; + let mut server = WebServer::new(Acceptor::bind_upgradable( + SelfContainedNetworkInterfaceListener::bind(80), + )); + match inner_main(&mut server, &config).await { + Ok(a) => { + server.shutdown().await; + Ok(a) + } Err(e) => { async { - tracing::error!("{}", e.source); - tracing::debug!("{:?}", e.source); - crate::sound::BEETHOVEN.play().await?; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); let ctx = DiagnosticContext::init( - cfg_path, - if tokio::fs::metadata("/media/embassy/config/disk.guid") + &config, + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_ok() { Some(Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), @@ -150,14 +179,9 @@ pub fn main() { None }, e, - ) - .await?; + )?; - let server = WebServer::diagnostic( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - ) - .await?; + server.serve_diagnostic(ctx.clone()); let mut shutdown = ctx.shutdown.subscribe(); @@ -166,7 +190,7 @@ pub fn main() { server.shutdown().await; - Ok::<_, Error>(shutdown) + Ok::<_, Error>(Some(shutdown)) } .await } diff --git a/core/startos/src/config/action.rs b/core/startos/src/config/action.rs deleted file mode 100644 index 27cd1683f..000000000 --- a/core/startos/src/config/action.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; - -use color_eyre::eyre::eyre; -use models::ImageId; -use patch_db::HasModel; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use super::{Config, ConfigSpec}; -use crate::context::RpcContext; -use crate::dependencies::Dependencies; -use crate::prelude::*; -use crate::procedure::docker::DockerContainers; -use crate::procedure::{PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::PackageId; -use crate::status::health_check::HealthCheckId; -use crate::util::Version; -use crate::volume::Volumes; -use crate::{Error, ResultExt}; - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConfigRes { - pub config: Option, - pub spec: ConfigSpec, -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct ConfigActions { - pub get: PackageProcedure, - pub set: PackageProcedure, -} -impl ConfigActions { - #[instrument(skip_all)] - pub fn validate( - &self, - _container: &Option, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - ) -> Result<(), Error> { - self.get - .validate(eos_version, volumes, image_ids, true) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Get"))?; - self.set - .validate(eos_version, volumes, image_ids, true) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Set"))?; - Ok(()) - } - #[instrument(skip_all)] - pub async fn get( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result { - self.get - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::GetConfig, - volumes, - None::<()>, - None, - ) - .await - .and_then(|res| { - res.map_err(|e| Error::new(eyre!("{}", e.1), crate::ErrorKind::ConfigGen)) - }) - } - - #[instrument(skip_all)] - pub async fn set( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - dependencies: &Dependencies, - volumes: &Volumes, - input: &Config, - ) -> Result { - let res: SetResult = self - .set - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::SetConfig, - volumes, - Some(input), - None, - ) - .await - .and_then(|res| { - res.map_err(|e| { - Error::new(eyre!("{}", e.1), crate::ErrorKind::ConfigRulesViolation) - }) - })?; - Ok(SetResult { - depends_on: res - .depends_on - .into_iter() - .filter(|(pkg, _)| dependencies.0.contains_key(pkg)) - .collect(), - }) - } -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct SetResult { - pub depends_on: BTreeMap>, -} diff --git a/core/startos/src/config/mod.rs b/core/startos/src/config/mod.rs deleted file mode 100644 index 06e7770b0..000000000 --- a/core/startos/src/config/mod.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use indexmap::IndexSet; -use itertools::Itertools; -use models::{ErrorKind, OptionExt}; -use patch_db::value::InternedString; -use patch_db::Value; -use regex::Regex; -use rpc_toolkit::command; -use tracing::instrument; - -use crate::context::RpcContext; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::display_none; -use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat}; -use crate::Error; - -pub mod action; -pub mod spec; -pub mod util; - -pub use spec::{ConfigSpec, Defaultable}; -use util::NumRange; - -use self::action::ConfigRes; -use self::spec::ValueSpecPointer; - -pub type Config = patch_db::value::InOMap; -pub trait TypeOf { - fn type_of(&self) -> &'static str; -} -impl TypeOf for Value { - fn type_of(&self) -> &'static str { - match self { - Value::Array(_) => "list", - Value::Bool(_) => "boolean", - Value::Null => "null", - Value::Number(_) => "number", - Value::Object(_) => "object", - Value::String(_) => "string", - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum ConfigurationError { - #[error("Timeout Error")] - TimeoutError(#[from] TimeoutError), - #[error("No Match: {0}")] - NoMatch(#[from] NoMatchWithPath), - #[error("System Error: {0}")] - SystemError(Error), - #[error("Permission Denied: {0}")] - PermissionDenied(ValueSpecPointer), -} -impl From for Error { - fn from(err: ConfigurationError) -> Self { - let kind = match &err { - ConfigurationError::SystemError(e) => e.kind, - _ => crate::ErrorKind::ConfigGen, - }; - crate::Error::new(err, kind) - } -} - -#[derive(Clone, Copy, Debug, thiserror::Error)] -#[error("Timeout Error")] -pub struct TimeoutError; - -#[derive(Clone, Debug, thiserror::Error)] -pub struct NoMatchWithPath { - pub path: Vec, - pub error: MatchError, -} -impl NoMatchWithPath { - pub fn new(error: MatchError) -> Self { - NoMatchWithPath { - path: Vec::new(), - error, - } - } - pub fn prepend(mut self, seg: InternedString) -> Self { - self.path.push(seg); - self - } -} -impl std::fmt::Display for NoMatchWithPath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}: {}", self.path.iter().rev().join("."), self.error) - } -} -impl From for Error { - fn from(e: NoMatchWithPath) -> Self { - ConfigurationError::from(e).into() - } -} - -#[derive(Clone, Debug, thiserror::Error)] -pub enum MatchError { - #[error("String {0:?} Does Not Match Pattern {1}")] - Pattern(Arc, Regex), - #[error("String {0:?} Is Not In Enum {1:?}")] - Enum(Arc, IndexSet), - #[error("Field Is Not Nullable")] - NotNullable, - #[error("Length Mismatch: expected {0}, actual: {1}")] - LengthMismatch(NumRange, usize), - #[error("Invalid Type: expected {0}, actual: {1}")] - InvalidType(&'static str, &'static str), - #[error("Number Out Of Range: expected {0}, actual: {1}")] - OutOfRange(NumRange, f64), - #[error("Number Is Not Integral: {0}")] - NonIntegral(f64), - #[error("Variant {0:?} Is Not In Union {1:?}")] - Union(Arc, IndexSet), - #[error("Variant Is Missing Tag {0:?}")] - MissingTag(InternedString), - #[error("Property {0:?} Of Variant {1:?} Conflicts With Union Tag")] - PropertyMatchesUnionTag(InternedString, String), - #[error("Name of Property {0:?} Conflicts With Map Tag Name")] - PropertyNameMatchesMapTag(String), - #[error("Pointer Is Invalid: {0}")] - InvalidPointer(spec::ValueSpecPointer), - #[error("Object Key Is Invalid: {0}")] - InvalidKey(String), - #[error("Value In List Is Not Unique")] - ListUniquenessViolation, -} - -#[command(rename = "config-spec", cli_only, blocking, display(display_none))] -pub fn verify_spec(#[arg] path: PathBuf) -> Result<(), Error> { - let mut file = std::fs::File::open(&path)?; - let format = match path.extension().and_then(|s| s.to_str()) { - Some("yaml") | Some("yml") => IoFormat::Yaml, - Some("json") => IoFormat::Json, - Some("toml") => IoFormat::Toml, - Some("cbor") => IoFormat::Cbor, - _ => { - return Err(Error::new( - eyre!("Unknown file format. Expected one of yaml, json, toml, cbor."), - crate::ErrorKind::Deserialization, - )); - } - }; - let _: ConfigSpec = format.from_reader(&mut file)?; - - Ok(()) -} - -#[command(subcommands(get, set))] -pub fn config(#[arg] id: PackageId) -> Result { - Ok(id) -} - -#[command(display(display_serializable))] -#[instrument(skip_all)] -pub async fn get( - #[context] ctx: RpcContext, - #[parent_data] id: PackageId, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { - let db = ctx.db.peek().await; - let manifest = db - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .as_installed() - .or_not_found(&id)? - .as_manifest(); - let action = manifest - .as_config() - .de()? - .ok_or_else(|| Error::new(eyre!("{} has no config", id), crate::ErrorKind::NotFound))?; - - let volumes = manifest.as_volumes().de()?; - let version = manifest.as_version().de()?; - action.get(&ctx, &id, &version, &volumes).await -} - -#[command( - subcommands(self(set_impl(async, context(RpcContext))), set_dry), - display(display_none), - metadata(sync_db = true) -)] -#[instrument(skip_all)] -pub fn set( - #[parent_data] id: PackageId, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, - #[arg(long = "timeout")] timeout: Option, - #[arg(stdin, parse(parse_stdin_deserializable))] config: Option, -) -> Result<(PackageId, Option, Option), Error> { - Ok((id, config, timeout.map(|d| *d))) -} - -#[command(rename = "dry", display(display_serializable))] -#[instrument(skip_all)] -pub async fn set_dry( - #[context] ctx: RpcContext, - #[parent_data] (id, config, timeout): (PackageId, Option, Option), -) -> Result, Error> { - let breakages = BTreeMap::new(); - let overrides = Default::default(); - - let configure_context = ConfigureContext { - breakages, - timeout, - config, - dry_run: true, - overrides, - }; - let breakages = configure(&ctx, &id, configure_context).await?; - - Ok(breakages) -} - -pub struct ConfigureContext { - pub breakages: BTreeMap, - pub timeout: Option, - pub config: Option, - pub overrides: BTreeMap, - pub dry_run: bool, -} - -#[instrument(skip_all)] -pub async fn set_impl( - ctx: RpcContext, - (id, config, timeout): (PackageId, Option, Option), -) -> Result<(), Error> { - let breakages = BTreeMap::new(); - let overrides = Default::default(); - - let configure_context = ConfigureContext { - breakages, - timeout, - config, - dry_run: false, - overrides, - }; - configure(&ctx, &id, configure_context).await?; - Ok(()) -} - -#[instrument(skip_all)] -pub async fn configure( - ctx: &RpcContext, - id: &PackageId, - configure_context: ConfigureContext, -) -> Result, Error> { - let db = ctx.db.peek().await; - let package = db - .as_package_data() - .as_idx(id) - .or_not_found(&id)? - .as_installed() - .or_not_found(&id)?; - let version = package.as_manifest().as_version().de()?; - ctx.managers - .get(&(id.clone(), version.clone())) - .await - .ok_or_else(|| { - Error::new( - eyre!("There is no manager running for {id:?} and {version:?}"), - ErrorKind::Unknown, - ) - })? - .configure(configure_context) - .await -} - -macro_rules! not_found { - ($x:expr) => { - crate::Error::new( - color_eyre::eyre::eyre!("Could not find {} at {}:{}", $x, module_path!(), line!()), - crate::ErrorKind::Incoherent, - ) - }; -} -pub(crate) use not_found; diff --git a/core/startos/src/config/spec.rs b/core/startos/src/config/spec.rs deleted file mode 100644 index a98ad888d..000000000 --- a/core/startos/src/config/spec.rs +++ /dev/null @@ -1,2013 +0,0 @@ -use std::borrow::Cow; -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; -use std::fmt::Debug; -use std::hash::{Hash, Hasher}; -use std::iter::FromIterator; -use std::ops::RangeBounds; -use std::sync::Arc; -use std::time::Duration; - -use async_trait::async_trait; -use imbl::Vector; -use imbl_value::InternedString; -use indexmap::{IndexMap, IndexSet}; -use itertools::Itertools; -use jsonpath_lib::Compiled as CompiledJsonPath; -use patch_db::value::{Number, Value}; -use rand::{CryptoRng, Rng}; -use regex::Regex; -use serde::de::{MapAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use sqlx::PgPool; - -use super::util::{self, CharSet, NumRange, UniqueBy, STATIC_NULL}; -use super::{Config, MatchError, NoMatchWithPath, TimeoutError, TypeOf}; -use crate::config::ConfigurationError; -use crate::context::RpcContext; -use crate::net::interface::InterfaceId; -use crate::net::keys::Key; -use crate::prelude::*; -use crate::s9pk::manifest::{Manifest, PackageId}; - -// Config Value Specifications -#[async_trait] -pub trait ValueSpec { - // This function defines whether the value supplied in the argument is - // consistent with the spec in &self - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath>; - // This function checks whether the value spec is consistent with itself, - // since not all inVariant can be checked by the type - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath>; - // update is to fill in values for environment pointers recursively - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError>; - // returns all pointers that are live in the provided config - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath>; - // requires returns whether the app id is the target of a pointer within it - fn requires(&self, id: &PackageId, value: &Value) -> bool; - // defines if 2 values of this type are equal for the purpose of uniqueness - fn eq(&self, lhs: &Value, rhs: &Value) -> bool; -} - -// Config Value Default Generation -// -// This behavior is defined by two independent traits as well as a third that -// represents a conjunction of those two traits: -// -// DefaultableWith - defines an associated type describing the information it -// needs to be able to generate a default value, as well as a function for -// extracting relevant pieces of that information and using it to actually -// generate the default value -// -// HasDefaultSpec - only purpose is to summon the default spec for the type -// -// Defaultable - this is a redundant trait that may replace 'DefaultableWith' -// and 'HasDefaultSpec'. -pub trait DefaultableWith { - type DefaultSpec: Sync; - type Error: std::error::Error; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result; -} -pub trait HasDefaultSpec: DefaultableWith { - fn default_spec(&self) -> &Self::DefaultSpec; -} - -pub trait Defaultable { - type Error; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result; -} -impl Defaultable for T -where - T: HasDefaultSpec + DefaultableWith + Sync, - E: std::error::Error, -{ - type Error = E; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.gen_with(self.default_spec(), rng, timeout) - } -} - -// WithDefault - trivial wrapper that pairs a 'DefaultableWith' type with a -// default spec -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WithDefault { - #[serde(flatten)] - pub inner: T, - pub default: T::DefaultSpec, -} -impl DefaultableWith for WithDefault -where - T: DefaultableWith + Sync + Send, - T::DefaultSpec: Send, -{ - type DefaultSpec = T::DefaultSpec; - type Error = T::Error; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.inner.gen_with(spec, rng, timeout) - } -} -impl HasDefaultSpec for WithDefault -where - T: DefaultableWith + Sync + Send, - T::DefaultSpec: Send, -{ - fn default_spec(&self) -> &Self::DefaultSpec { - &self.default - } -} -#[async_trait] -impl ValueSpec for WithDefault -where - T: ValueSpec + DefaultableWith + Send + Sync, - Self: Send + Sync, -{ - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - self.inner.matches(value) - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - self.inner.validate(manifest) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - self.inner - .update(ctx, manifest, config_overrides, value) - .await - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - self.inner.pointers(value) - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - self.inner.requires(id, value) - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - self.inner.eq(lhs, rhs) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WithNullable { - #[serde(flatten)] - pub inner: T, - pub nullable: bool, -} -#[async_trait] -impl ValueSpec for WithNullable -where - T: ValueSpec + Send + Sync, - Self: Send + Sync, -{ - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match (self.nullable, value) { - (true, &Value::Null) => Ok(()), - _ => self.inner.matches(value), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - self.inner.validate(manifest) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - self.inner - .update(ctx, manifest, config_overrides, value) - .await - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - self.inner.pointers(value) - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - self.inner.requires(id, value) - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - self.inner.eq(lhs, rhs) - } -} - -impl DefaultableWith for WithNullable -where - T: DefaultableWith + Sync + Send, -{ - type DefaultSpec = T::DefaultSpec; - type Error = T::Error; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.inner.gen_with(spec, rng, timeout) - } -} - -impl Defaultable for WithNullable -where - T: Defaultable + Sync + Send, -{ - type Error = T::Error; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.inner.gen(rng, timeout) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct WithDescription { - #[serde(flatten)] - pub inner: T, - pub description: Option, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub warning: Option, -} -#[async_trait] -impl ValueSpec for WithDescription -where - T: ValueSpec + Sync + Send, - Self: Sync + Send, -{ - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - self.inner.matches(value) - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - self.inner.validate(manifest) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - self.inner - .update(ctx, manifest, config_overrides, value) - .await - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - self.inner.pointers(value) - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - self.inner.requires(id, value) - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - self.inner.eq(lhs, rhs) - } -} - -impl DefaultableWith for WithDescription -where - T: DefaultableWith + Sync + Send, -{ - type DefaultSpec = T::DefaultSpec; - type Error = T::Error; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.inner.gen_with(spec, rng, timeout) - } -} - -impl Defaultable for WithDescription -where - T: Defaultable + Sync + Send, -{ - type Error = T::Error; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.inner.gen(rng, timeout) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "type")] -pub enum ValueSpecAny { - Boolean(WithDescription>), - Enum(WithDescription>), - List(ValueSpecList), - Number(WithDescription>>), - Object(WithDescription), - String(WithDescription>>), - Union(WithDescription>), - Pointer(WithDescription), -} -impl ValueSpecAny { - pub fn name(&self) -> &'_ str { - match self { - ValueSpecAny::Boolean(b) => b.name.as_str(), - ValueSpecAny::Enum(e) => e.name.as_str(), - ValueSpecAny::List(l) => match l { - ValueSpecList::Enum(e) => e.name.as_str(), - ValueSpecList::Number(n) => n.name.as_str(), - ValueSpecList::Object(o) => o.name.as_str(), - ValueSpecList::String(s) => s.name.as_str(), - ValueSpecList::Union(u) => u.name.as_str(), - }, - ValueSpecAny::Number(n) => n.name.as_str(), - ValueSpecAny::Object(o) => o.name.as_str(), - ValueSpecAny::Pointer(p) => p.name.as_str(), - ValueSpecAny::String(s) => s.name.as_str(), - ValueSpecAny::Union(u) => u.name.as_str(), - } - } -} -#[async_trait] -impl ValueSpec for ValueSpecAny { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecAny::Boolean(a) => a.matches(value), - ValueSpecAny::Enum(a) => a.matches(value), - ValueSpecAny::List(a) => a.matches(value), - ValueSpecAny::Number(a) => a.matches(value), - ValueSpecAny::Object(a) => a.matches(value), - ValueSpecAny::String(a) => a.matches(value), - ValueSpecAny::Union(a) => a.matches(value), - ValueSpecAny::Pointer(a) => a.matches(value), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecAny::Boolean(a) => a.validate(manifest), - ValueSpecAny::Enum(a) => a.validate(manifest), - ValueSpecAny::List(a) => a.validate(manifest), - ValueSpecAny::Number(a) => a.validate(manifest), - ValueSpecAny::Object(a) => a.validate(manifest), - ValueSpecAny::String(a) => a.validate(manifest), - ValueSpecAny::Union(a) => a.validate(manifest), - ValueSpecAny::Pointer(a) => a.validate(manifest), - } - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - match self { - ValueSpecAny::Boolean(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::Enum(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::List(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::Number(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::Object(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::String(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::Union(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecAny::Pointer(a) => a.update(ctx, manifest, config_overrides, value).await, - } - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - match self { - ValueSpecAny::Boolean(a) => a.pointers(value), - ValueSpecAny::Enum(a) => a.pointers(value), - ValueSpecAny::List(a) => a.pointers(value), - ValueSpecAny::Number(a) => a.pointers(value), - ValueSpecAny::Object(a) => a.pointers(value), - ValueSpecAny::String(a) => a.pointers(value), - ValueSpecAny::Union(a) => a.pointers(value), - ValueSpecAny::Pointer(a) => a.pointers(value), - } - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - match self { - ValueSpecAny::Boolean(a) => a.requires(id, value), - ValueSpecAny::Enum(a) => a.requires(id, value), - ValueSpecAny::List(a) => a.requires(id, value), - ValueSpecAny::Number(a) => a.requires(id, value), - ValueSpecAny::Object(a) => a.requires(id, value), - ValueSpecAny::String(a) => a.requires(id, value), - ValueSpecAny::Union(a) => a.requires(id, value), - ValueSpecAny::Pointer(a) => a.requires(id, value), - } - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match self { - ValueSpecAny::Boolean(a) => a.eq(lhs, rhs), - ValueSpecAny::Enum(a) => a.eq(lhs, rhs), - ValueSpecAny::List(a) => a.eq(lhs, rhs), - ValueSpecAny::Number(a) => a.eq(lhs, rhs), - ValueSpecAny::Object(a) => a.eq(lhs, rhs), - ValueSpecAny::String(a) => a.eq(lhs, rhs), - ValueSpecAny::Union(a) => a.eq(lhs, rhs), - ValueSpecAny::Pointer(a) => a.eq(lhs, rhs), - } - } -} -impl Defaultable for ValueSpecAny { - type Error = ConfigurationError; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - match self { - ValueSpecAny::Boolean(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), - ValueSpecAny::Enum(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), - ValueSpecAny::List(a) => a.gen(rng, timeout), - ValueSpecAny::Number(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), - ValueSpecAny::Object(a) => a.gen(rng, timeout), - ValueSpecAny::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), - ValueSpecAny::Union(a) => a.gen(rng, timeout), - ValueSpecAny::Pointer(a) => a.gen(rng, timeout), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ValueSpecBoolean {} -#[async_trait] -impl ValueSpec for ValueSpecBoolean { - fn matches(&self, val: &Value) -> Result<(), NoMatchWithPath> { - match val { - Value::Bool(_) => Ok(()), - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "boolean", - a.type_of(), - ))), - } - } - fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { - Ok(()) - } - async fn update( - &self, - _ctx: &RpcContext, - _manifest: &Manifest, - _config_overrides: &BTreeMap, - _value: &mut Value, - ) -> Result<(), ConfigurationError> { - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - Ok(BTreeSet::new()) - } - fn requires(&self, _id: &PackageId, _value: &Value) -> bool { - false - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::Bool(lhs), Value::Bool(rhs)) => lhs == rhs, - _ => false, - } - } -} -impl DefaultableWith for ValueSpecBoolean { - type DefaultSpec = bool; - type Error = crate::util::Never; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::Bool(*spec)) - } -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ValueSpecEnum { - pub values: IndexSet, - pub value_names: BTreeMap, -} -impl<'de> serde::de::Deserialize<'de> for ValueSpecEnum { - fn deserialize>(deserializer: D) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct _ValueSpecEnum { - pub values: IndexSet, - #[serde(default)] - pub value_names: BTreeMap, - } - - let mut r#enum = _ValueSpecEnum::deserialize(deserializer)?; - for name in &r#enum.values { - if !r#enum.value_names.contains_key(name) { - r#enum.value_names.insert(name.clone(), name.clone()); - } - } - Ok(ValueSpecEnum { - values: r#enum.values, - value_names: r#enum.value_names, - }) - } -} -#[async_trait] -impl ValueSpec for ValueSpecEnum { - fn matches(&self, val: &Value) -> Result<(), NoMatchWithPath> { - match val { - Value::String(b) => { - if self.values.contains(&**b) { - Ok(()) - } else { - Err(NoMatchWithPath::new(MatchError::Enum( - b.clone(), - self.values.clone(), - ))) - } - } - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "string", - a.type_of(), - ))), - } - } - fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { - Ok(()) - } - async fn update( - &self, - _ctx: &RpcContext, - _manifest: &Manifest, - _config_overrides: &BTreeMap, - _value: &mut Value, - ) -> Result<(), ConfigurationError> { - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - Ok(BTreeSet::new()) - } - fn requires(&self, _id: &PackageId, _value: &Value) -> bool { - false - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::String(lhs), Value::String(rhs)) => lhs == rhs, - _ => false, - } - } -} -impl DefaultableWith for ValueSpecEnum { - type DefaultSpec = Arc; - type Error = crate::util::Never; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::String(spec.clone())) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ListSpec { - pub spec: T, - pub range: NumRange, -} -#[async_trait] -impl ValueSpec for ListSpec -where - T: ValueSpec + Sync + Send, - Self: Sync + Send, -{ - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match value { - Value::Array(l) => { - if !self.range.contains(&l.len()) { - Err(NoMatchWithPath { - path: Vec::new(), - error: MatchError::LengthMismatch(self.range.clone(), l.len()), - }) - } else { - l.iter() - .enumerate() - .map(|(i, v)| { - self.spec - .matches(v) - .map_err(|e| e.prepend(InternedString::from_display(&i)))?; - if l.iter() - .enumerate() - .any(|(i2, v2)| i != i2 && self.spec.eq(v, v2)) - { - Err(NoMatchWithPath::new(MatchError::ListUniquenessViolation) - .prepend(InternedString::from_display(&i))) - } else { - Ok(()) - } - }) - .collect() - } - } - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "list", - a.type_of(), - ))), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - self.spec.validate(manifest) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - if let Value::Array(ref mut ls) = value { - for (i, val) in ls.iter_mut().enumerate() { - match self.spec.update(ctx, manifest, config_overrides, val).await { - Err(ConfigurationError::NoMatch(e)) => Err(ConfigurationError::NoMatch( - e.prepend(InternedString::from_display(&i)), - )), - a => a, - }?; - } - Ok(()) - } else { - Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::InvalidType("list", value.type_of()), - ))) - } - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - Ok(BTreeSet::new()) - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - if let Value::Array(ref ls) = value { - ls.into_iter().any(|v| self.spec.requires(id, v)) - } else { - false - } - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::Array(lhs), Value::Array(rhs)) => { - lhs.iter().zip_longest(rhs.iter()).all(|zip| match zip { - itertools::EitherOrBoth::Both(lhs, rhs) => lhs == rhs, - _ => false, - }) - } - _ => false, - } - } -} - -impl DefaultableWith for ListSpec -where - T: DefaultableWith + Sync + Send, -{ - type DefaultSpec = Vec; - type Error = T::Error; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - let mut res = Vector::new(); - for spec_member in spec.iter() { - res.push_back(self.spec.gen_with(spec_member, rng, timeout)?); - } - Ok(Value::Array(res)) - } -} - -unsafe impl Sync for ValueSpecObject {} // TODO: remove -unsafe impl Send for ValueSpecObject {} // TODO: remove -unsafe impl Sync for ValueSpecUnion {} // TODO: remove -unsafe impl Send for ValueSpecUnion {} // TODO: remove - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "subtype")] -pub enum ValueSpecList { - Enum(WithDescription>>), - Number(WithDescription>>), - Object(WithDescription>>), - String(WithDescription>>), - Union(WithDescription>>>), -} -#[async_trait] -impl ValueSpec for ValueSpecList { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecList::Enum(a) => a.matches(value), - ValueSpecList::Number(a) => a.matches(value), - ValueSpecList::Object(a) => a.matches(value), - ValueSpecList::String(a) => a.matches(value), - ValueSpecList::Union(a) => a.matches(value), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecList::Enum(a) => a.validate(manifest), - ValueSpecList::Number(a) => a.validate(manifest), - ValueSpecList::Object(a) => a.validate(manifest), - ValueSpecList::String(a) => a.validate(manifest), - ValueSpecList::Union(a) => a.validate(manifest), - } - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - match self { - ValueSpecList::Enum(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecList::Number(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecList::Object(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecList::String(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecList::Union(a) => a.update(ctx, manifest, config_overrides, value).await, - } - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - match self { - ValueSpecList::Enum(a) => a.pointers(value), - ValueSpecList::Number(a) => a.pointers(value), - ValueSpecList::Object(a) => a.pointers(value), - ValueSpecList::String(a) => a.pointers(value), - ValueSpecList::Union(a) => a.pointers(value), - } - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - match self { - ValueSpecList::Enum(a) => a.requires(id, value), - ValueSpecList::Number(a) => a.requires(id, value), - ValueSpecList::Object(a) => a.requires(id, value), - ValueSpecList::String(a) => a.requires(id, value), - ValueSpecList::Union(a) => a.requires(id, value), - } - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match self { - ValueSpecList::Enum(a) => a.eq(lhs, rhs), - ValueSpecList::Number(a) => a.eq(lhs, rhs), - ValueSpecList::Object(a) => a.eq(lhs, rhs), - ValueSpecList::String(a) => a.eq(lhs, rhs), - ValueSpecList::Union(a) => a.eq(lhs, rhs), - } - } -} - -impl Defaultable for ValueSpecList { - type Error = ConfigurationError; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - match self { - ValueSpecList::Enum(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), - ValueSpecList::Number(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), - ValueSpecList::Object(a) => { - let mut ret = match a.gen(rng, timeout).unwrap() { - Value::Array(l) => l, - a => { - return Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::InvalidType("list", a.type_of()), - ))) - } - }; - while !( - a.inner.inner.range.start_bound(), - std::ops::Bound::Unbounded, - ) - .contains(&ret.len()) - { - ret.push_back( - a.inner - .inner - .spec - .gen(rng, timeout) - .map_err(ConfigurationError::from)?, - ); - } - Ok(Value::Array(ret)) - } - ValueSpecList::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), - ValueSpecList::Union(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ValueSpecNumber { - range: Option>, - #[serde(default)] - integral: bool, - #[serde(skip_serializing_if = "Option::is_none")] - units: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - pub placeholder: Option, -} -#[async_trait] -impl ValueSpec for ValueSpecNumber { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match value { - Value::Number(n) => { - let n = n.as_f64().unwrap(); - if self.integral && n.floor() != n { - return Err(NoMatchWithPath::new(MatchError::NonIntegral(n))); - } - if let Some(range) = &self.range { - if !range.contains(&n) { - return Err(NoMatchWithPath::new(MatchError::OutOfRange( - range.clone(), - n, - ))); - } - } - Ok(()) - } - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "object", - a.type_of(), - ))), - } - } - fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { - Ok(()) - } - async fn update( - &self, - _ctx: &RpcContext, - _manifest: &Manifest, - _config_overrides: &BTreeMap, - _value: &mut Value, - ) -> Result<(), ConfigurationError> { - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - Ok(BTreeSet::new()) - } - fn requires(&self, _id: &PackageId, _value: &Value) -> bool { - false - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::Number(lhs), Value::Number(rhs)) => lhs == rhs, - _ => false, - } - } -} -impl DefaultableWith for ValueSpecNumber { - type DefaultSpec = Option; - type Error = crate::util::Never; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(spec - .clone() - .map(|s| Value::Number(s)) - .unwrap_or(Value::Null)) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct ValueSpecObject { - pub spec: ConfigSpec, - pub display_as: Option, - #[serde(default)] - pub unique_by: UniqueBy, -} -#[async_trait] -impl ValueSpec for ValueSpecObject { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match value { - Value::Object(o) => self.spec.matches(o), - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "object", - a.type_of(), - ))), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - self.spec.validate(manifest) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - if let Value::Object(o) = value { - self.spec.update(ctx, manifest, config_overrides, o).await - } else { - Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::InvalidType("object", value.type_of()), - ))) - } - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - if let Value::Object(o) = value { - self.spec.pointers(o) - } else { - Err(NoMatchWithPath::new(MatchError::InvalidType( - "object", - value.type_of(), - ))) - } - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - if let Value::Object(o) = value { - self.spec.requires(id, o) - } else { - false - } - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::Object(lhs), Value::Object(rhs)) => self.unique_by.eq(lhs, rhs), - _ => false, - } - } -} -impl DefaultableWith for ValueSpecObject { - type DefaultSpec = Config; - type Error = crate::util::Never; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::Object(spec.clone())) - } -} -impl Defaultable for ValueSpecObject { - type Error = ConfigurationError; - - fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - self.spec.gen(rng, timeout).map(Value::Object) - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct ConfigSpec(pub IndexMap); -impl ConfigSpec { - pub fn matches(&self, value: &Config) -> Result<(), NoMatchWithPath> { - for (key, val) in self.0.iter() { - if let Some(v) = value.get(&**key) { - val.matches(v).map_err(|e| e.prepend(key.clone()))?; - } else { - val.matches(&Value::Null) - .map_err(|e| e.prepend(key.clone()))?; - } - } - Ok(()) - } - - pub fn gen( - &self, - rng: &mut R, - timeout: &Option, - ) -> Result { - let mut res = Config::new(); - for (key, val) in self.0.iter() { - res.insert(key.clone(), val.gen(rng, timeout)?); - } - Ok(res) - } - - pub fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - for (name, val) in &self.0 { - val.validate(manifest) - .map_err(|e| e.prepend(name.clone()))?; - } - Ok(()) - } - - pub async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - cfg: &mut Config, - ) -> Result<(), ConfigurationError> { - for (k, vs) in self.0.iter() { - match cfg.get_mut(k) { - None => { - let mut v = Value::Null; - vs.update(ctx, manifest, config_overrides, &mut v).await?; - cfg.insert(k.clone(), v); - } - Some(v) => match vs.update(ctx, manifest, config_overrides, v).await { - Err(ConfigurationError::NoMatch(e)) => { - Err(ConfigurationError::NoMatch(e.prepend(k.clone()))) - } - a => a, - }?, - }; - } - Ok(()) - } - - pub fn pointers(&self, cfg: &Config) -> Result, NoMatchWithPath> { - cfg.iter() - .filter_map(|(k, v)| self.0.get(k).map(|vs| (k, vs.pointers(v)))) - .fold(Ok(BTreeSet::::new()), |acc, v| { - match (acc, v) { - // propagate existing errors - (Err(e), _) => Err(e), - // create new error case - (Ok(_), (k, Err(e))) => Err(e.prepend(k.clone())), - // combine sets - (Ok(s0), (_, Ok(s1))) => Ok(BTreeSet::from_iter(s0.union(&s1).cloned())), - } - }) - } - - pub fn requires(&self, id: &PackageId, cfg: &Config) -> bool { - self.0 - .iter() - .any(|(k, v)| v.requires(id, cfg.get(k).unwrap_or(&STATIC_NULL))) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Pattern { - #[serde(with = "util::serde_regex")] - pub pattern: Regex, - pub pattern_description: String, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ValueSpecString { - #[serde(flatten)] - pub pattern: Option, - pub textarea: bool, - pub copyable: bool, - pub masked: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub placeholder: Option, -} -impl<'de> Deserialize<'de> for ValueSpecString { - fn deserialize>(deserializer: D) -> Result { - struct ValueSpecStringVisitor; - impl<'de> Visitor<'de> for ValueSpecStringVisitor { - type Value = ValueSpecString; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct ValueSpecString") - } - fn visit_map>(self, mut map: V) -> Result { - let mut pattern = None; - let mut pattern_description = None; - let mut textarea = false; - let mut copyable = false; - let mut masked = false; - let mut placeholder = None; - while let Some::(key) = map.next_key()? { - if &key == "pattern" { - if pattern.is_some() { - return Err(serde::de::Error::duplicate_field("pattern")); - } else { - pattern = Some( - Regex::new(&map.next_value::()?) - .map_err(serde::de::Error::custom)?, - ); - } - } else if &key == "pattern-description" { - if pattern_description.is_some() { - return Err(serde::de::Error::duplicate_field("pattern-description")); - } else { - pattern_description = Some(map.next_value()?); - } - } else if &key == "textarea" { - textarea = map.next_value()?; - } else if &key == "copyable" { - copyable = map.next_value()?; - } else if &key == "masked" { - masked = map.next_value()?; - } else if &key == "placeholder" { - if placeholder.is_some() { - return Err(serde::de::Error::duplicate_field("placeholder")); - } else { - placeholder = Some(map.next_value()?); - } - } - } - let regex = match (pattern, pattern_description) { - (None, None) => None, - (Some(p), Some(d)) => Some(Pattern { - pattern: p, - pattern_description: d, - }), - (Some(_), None) => { - return Err(serde::de::Error::missing_field("pattern-description")); - } - (None, Some(_)) => { - return Err(serde::de::Error::missing_field("pattern")); - } - }; - Ok(ValueSpecString { - pattern: regex, - textarea, - copyable, - masked, - placeholder, - }) - } - } - const FIELDS: &[&str] = &[ - "pattern", - "pattern-description", - "textarea", - "copyable", - "masked", - "placeholder", - ]; - deserializer.deserialize_struct("ValueSpecString", FIELDS, ValueSpecStringVisitor) - } -} -#[async_trait] -impl ValueSpec for ValueSpecString { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match value { - Value::String(s) => { - if let Some(pattern) = &self.pattern { - if pattern.pattern.is_match(s) { - Ok(()) - } else { - Err(NoMatchWithPath::new(MatchError::Pattern( - s.clone(), - pattern.pattern.clone(), - ))) - } - } else { - Ok(()) - } - } - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "string", - a.type_of(), - ))), - } - } - fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { - Ok(()) - } - async fn update( - &self, - _ctx: &RpcContext, - _manifest: &Manifest, - _config_overrides: &BTreeMap, - _value: &mut Value, - ) -> Result<(), ConfigurationError> { - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - Ok(BTreeSet::new()) - } - fn requires(&self, _id: &PackageId, _value: &Value) -> bool { - false - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::String(lhs), Value::String(rhs)) => lhs == rhs, - _ => false, - } - } -} -impl DefaultableWith for ValueSpecString { - type DefaultSpec = Option; - type Error = TimeoutError; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - if let Some(spec) = spec { - let now = timeout.as_ref().map(|_| std::time::Instant::now()); - loop { - let candidate = spec.gen(rng); - match (spec, &self.pattern) { - (DefaultString::Entropy(_), Some(pattern)) - if !pattern.pattern.is_match(&candidate) => {} - _ => { - return Ok(Value::String(candidate)); - } - } - if let (Some(now), Some(timeout)) = (now, timeout) { - if &now.elapsed() > timeout { - return Err(TimeoutError); - } - } else { - return Ok(Value::String(candidate)); - } - } - } else { - Ok(Value::Null) - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum DefaultString { - Literal(String), - Entropy(Entropy), -} -impl DefaultString { - pub fn gen(&self, rng: &mut R) -> Arc { - Arc::new(match self { - DefaultString::Literal(s) => s.clone(), - DefaultString::Entropy(e) => e.gen(rng), - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Entropy { - pub charset: Option, - pub len: usize, -} -impl Entropy { - pub fn gen(&self, rng: &mut R) -> String { - let len = self.len; - let set = self - .charset - .as_ref() - .map(|cs| Cow::Borrowed(cs)) - .unwrap_or_else(|| Cow::Owned(Default::default())); - std::iter::repeat_with(|| set.gen(rng)).take(len).collect() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct UnionTag { - pub id: InternedString, - pub name: String, - pub description: Option, - pub variant_names: BTreeMap, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ValueSpecUnion { - pub tag: UnionTag, - pub variants: BTreeMap, - pub display_as: Option, - pub unique_by: UniqueBy, -} - -impl<'de> serde::de::Deserialize<'de> for ValueSpecUnion { - fn deserialize>(deserializer: D) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "kebab-case")] - #[serde(untagged)] - pub enum _UnionTag { - Old(InternedString), - New(UnionTag), - } - #[derive(Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct _ValueSpecUnion { - pub variants: BTreeMap, - pub tag: _UnionTag, - pub display_as: Option, - #[serde(default)] - pub unique_by: UniqueBy, - } - - let u = _ValueSpecUnion::deserialize(deserializer)?; - Ok(ValueSpecUnion { - tag: match u.tag { - _UnionTag::Old(id) => UnionTag { - id: id.clone(), - name: id.to_string(), - description: None, - variant_names: u - .variants - .keys() - .map(|k| (k.to_owned(), k.to_owned())) - .collect(), - }, - _UnionTag::New(UnionTag { - id, - name, - description, - mut variant_names, - }) => UnionTag { - id, - name, - description, - variant_names: { - let mut iter = u.variants.keys(); - while variant_names.len() < u.variants.len() { - if let Some(variant) = iter.next() { - variant_names.insert(variant.to_owned(), variant.to_owned()); - } else { - break; - } - } - variant_names - }, - }, - }, - variants: u.variants, - display_as: u.display_as, - unique_by: u.unique_by, - }) - } -} - -#[async_trait] -impl ValueSpec for ValueSpecUnion { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match value { - Value::Object(o) => { - if let Some(Value::String(ref tag)) = o.get(&*self.tag.id) { - if let Some(obj_spec) = self.variants.get(&**tag) { - let mut without_tag = o.clone(); - without_tag.remove(&*self.tag.id); - obj_spec.matches(&without_tag) - } else { - Err(NoMatchWithPath::new(MatchError::Union( - tag.clone(), - self.variants.keys().cloned().collect(), - ))) - } - } else { - Err(NoMatchWithPath::new(MatchError::MissingTag( - self.tag.id.clone(), - ))) - } - } - Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), - a => Err(NoMatchWithPath::new(MatchError::InvalidType( - "object", - a.type_of(), - ))), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - for (name, variant) in &self.variants { - if variant.0.get(&*self.tag.id).is_some() { - return Err(NoMatchWithPath::new(MatchError::PropertyMatchesUnionTag( - self.tag.id.clone(), - name.clone(), - ))); - } - variant.validate(manifest)?; - } - Ok(()) - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - if let Value::Object(o) = value { - match o.get(&*self.tag.id) { - None => Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::MissingTag(self.tag.id.clone()), - ))), - Some(Value::String(tag)) => match self.variants.get(&**tag) { - None => Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::Union(tag.clone(), self.variants.keys().cloned().collect()), - ))), - Some(spec) => spec.update(ctx, manifest, config_overrides, o).await, - }, - Some(other) => Err(ConfigurationError::NoMatch( - NoMatchWithPath::new(MatchError::InvalidType("string", other.type_of())) - .prepend(self.tag.id.clone()), - )), - } - } else { - Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::InvalidType("object", value.type_of()), - ))) - } - } - fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { - if let Value::Object(o) = value { - match o.get(&*self.tag.id) { - None => Err(NoMatchWithPath::new(MatchError::MissingTag( - self.tag.id.clone(), - ))), - Some(Value::String(tag)) => match self.variants.get(&**tag) { - None => Err(NoMatchWithPath::new(MatchError::Union( - tag.clone(), - self.variants.keys().cloned().collect(), - ))), - Some(spec) => spec.pointers(o), - }, - Some(other) => Err(NoMatchWithPath::new(MatchError::InvalidType( - "string", - other.type_of(), - )) - .prepend(self.tag.id.clone())), - } - } else { - Err(NoMatchWithPath::new(MatchError::InvalidType( - "object", - value.type_of(), - ))) - } - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - if let Value::Object(o) = value { - match o.get(&*self.tag.id) { - Some(Value::String(tag)) => match self.variants.get(&**tag) { - None => false, - Some(spec) => spec.requires(id, o), - }, - _ => false, - } - } else { - false - } - } - fn eq(&self, lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - (Value::Object(lhs), Value::Object(rhs)) => self.unique_by.eq(lhs, rhs), - _ => false, - } - } -} -impl DefaultableWith for ValueSpecUnion { - type DefaultSpec = Arc; - type Error = ConfigurationError; - - fn gen_with( - &self, - spec: &Self::DefaultSpec, - rng: &mut R, - timeout: &Option, - ) -> Result { - let variant = if let Some(v) = self.variants.get(&**spec) { - v - } else { - return Err(ConfigurationError::NoMatch(NoMatchWithPath::new( - MatchError::Union(spec.clone(), self.variants.keys().cloned().collect()), - ))); - }; - let cfg_res = variant.gen(rng, timeout)?; - - let mut tagged_cfg = Config::new(); - tagged_cfg.insert(self.tag.id.clone(), Value::String(spec.clone())); - tagged_cfg.extend(cfg_res.into_iter()); - - Ok(Value::Object(tagged_cfg)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(tag = "subtype")] -#[serde(rename_all = "kebab-case")] -pub enum ValueSpecPointer { - Package(PackagePointerSpec), - System(SystemPointerSpec), -} -impl fmt::Display for ValueSpecPointer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ValueSpecPointer::Package(p) => write!(f, "{}", p), - ValueSpecPointer::System(p) => write!(f, "{}", p), - } - } -} -impl Defaultable for ValueSpecPointer { - type Error = ConfigurationError; - fn gen( - &self, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::Null) - } -} -#[async_trait] -impl ValueSpec for ValueSpecPointer { - fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecPointer::Package(a) => a.matches(value), - ValueSpecPointer::System(a) => a.matches(value), - } - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - match self { - ValueSpecPointer::Package(a) => a.validate(manifest), - ValueSpecPointer::System(a) => a.validate(manifest), - } - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - match self { - ValueSpecPointer::Package(a) => a.update(ctx, manifest, config_overrides, value).await, - ValueSpecPointer::System(a) => a.update(ctx, manifest, config_overrides, value).await, - } - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - let mut pointers = BTreeSet::new(); - pointers.insert(self.clone()); - Ok(pointers) - } - fn requires(&self, id: &PackageId, value: &Value) -> bool { - match self { - ValueSpecPointer::Package(a) => a.requires(id, value), - ValueSpecPointer::System(a) => a.requires(id, value), - } - } - fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { - false - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(tag = "target")] -#[serde(rename_all = "kebab-case")] -pub enum PackagePointerSpec { - TorKey(TorKeyPointer), - TorAddress(TorAddressPointer), - LanAddress(LanAddressPointer), - Config(ConfigPointer), -} -impl PackagePointerSpec { - pub fn package_id(&self) -> &PackageId { - match self { - PackagePointerSpec::TorKey(TorKeyPointer { package_id, .. }) => package_id, - PackagePointerSpec::TorAddress(TorAddressPointer { package_id, .. }) => package_id, - PackagePointerSpec::LanAddress(LanAddressPointer { package_id, .. }) => package_id, - PackagePointerSpec::Config(ConfigPointer { package_id, .. }) => package_id, - } - } - async fn deref( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - ) -> Result { - match &self { - PackagePointerSpec::TorKey(key) => key.deref(&manifest.id, &ctx.secret_store).await, - PackagePointerSpec::TorAddress(tor) => tor.deref(ctx).await, - PackagePointerSpec::LanAddress(lan) => lan.deref(ctx).await, - PackagePointerSpec::Config(cfg) => cfg.deref(ctx, config_overrides).await, - } - } -} -impl fmt::Display for PackagePointerSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - PackagePointerSpec::TorKey(key) => write!(f, "{}", key), - PackagePointerSpec::TorAddress(tor) => write!(f, "{}", tor), - PackagePointerSpec::LanAddress(lan) => write!(f, "{}", lan), - PackagePointerSpec::Config(cfg) => write!(f, "{}", cfg), - } - } -} -impl Defaultable for PackagePointerSpec { - type Error = ConfigurationError; - fn gen( - &self, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::Null) - } -} -#[async_trait] -impl ValueSpec for PackagePointerSpec { - fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { - Ok(()) - } - fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { - if &manifest.id != self.package_id() - && !manifest.dependencies.0.contains_key(self.package_id()) - { - return Err(NoMatchWithPath::new(MatchError::InvalidPointer( - ValueSpecPointer::Package(self.clone()), - ))); - } - match self { - _ => Ok(()), - } - } - async fn update( - &self, - ctx: &RpcContext, - manifest: &Manifest, - config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - *value = self.deref(ctx, manifest, config_overrides).await?; - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - let mut pointers = BTreeSet::new(); - pointers.insert(ValueSpecPointer::Package(self.clone())); - Ok(pointers) - } - fn requires(&self, id: &PackageId, _value: &Value) -> bool { - self.package_id() == id - } - fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { - false - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct TorAddressPointer { - pub package_id: PackageId, - interface: InterfaceId, -} -impl TorAddressPointer { - async fn deref(&self, ctx: &RpcContext) -> Result { - let addr = ctx - .db - .peek() - .await - .as_package_data() - .as_idx(&self.package_id) - .and_then(|pde| pde.as_installed()) - .and_then(|i| i.as_interface_addresses().as_idx(&self.interface)) - .and_then(|a| a.as_tor_address().de().transpose()) - .transpose() - .map_err(|e| ConfigurationError::SystemError(e))?; - Ok(addr.map(Arc::new).map(Value::String).unwrap_or(Value::Null)) - } -} -impl fmt::Display for TorAddressPointer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TorAddressPointer { - package_id, - interface, - } => write!(f, "{}: tor-address: {}", package_id, interface), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct LanAddressPointer { - pub package_id: PackageId, - interface: InterfaceId, -} -impl fmt::Display for LanAddressPointer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let LanAddressPointer { - package_id, - interface, - } = self; - write!(f, "{}: lan-address: {}", package_id, interface) - } -} -impl LanAddressPointer { - async fn deref(&self, ctx: &RpcContext) -> Result { - let addr = ctx - .db - .peek() - .await - .as_package_data() - .as_idx(&self.package_id) - .and_then(|pde| pde.as_installed()) - .and_then(|i| i.as_interface_addresses().as_idx(&self.interface)) - .and_then(|a| a.as_lan_address().de().transpose()) - .transpose() - .map_err(|e| ConfigurationError::SystemError(e))?; - Ok(addr - .to_owned() - .map(Arc::new) - .map(Value::String) - .unwrap_or(Value::Null)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConfigPointer { - package_id: PackageId, - selector: Arc, - multi: bool, -} -impl ConfigPointer { - pub fn select(&self, val: &Value) -> Value { - self.selector.select(self.multi, val) - } - async fn deref( - &self, - ctx: &RpcContext, - config_overrides: &BTreeMap, - ) -> Result { - if let Some(cfg) = config_overrides.get(&self.package_id) { - Ok(self.select(&Value::Object(cfg.clone()))) - } else { - let id = &self.package_id; - let db = ctx.db.peek().await; - let manifest = db.as_package_data().as_idx(id).map(|pde| pde.as_manifest()); - let cfg_actions = manifest.and_then(|m| m.as_config().transpose_ref()); - if let (Some(manifest), Some(cfg_actions)) = (manifest, cfg_actions) { - let cfg_res = cfg_actions - .de() - .map_err(|e| ConfigurationError::SystemError(e))? - .get( - ctx, - &self.package_id, - &manifest - .as_version() - .de() - .map_err(|e| ConfigurationError::SystemError(e))?, - &manifest - .as_volumes() - .de() - .map_err(|e| ConfigurationError::SystemError(e))?, - ) - .await - .map_err(|e| ConfigurationError::SystemError(e))?; - if let Some(cfg) = cfg_res.config { - Ok(self.select(&Value::Object(cfg))) - } else { - Ok(Value::Null) - } - } else { - Ok(Value::Null) - } - } - } -} -impl fmt::Display for ConfigPointer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let ConfigPointer { - package_id, - selector, - .. - } = self; - write!(f, "{}: config: {}", package_id, selector) - } -} - -#[derive(Clone, Debug)] -pub struct ConfigSelector { - src: String, - compiled: CompiledJsonPath, -} -impl ConfigSelector { - fn select(&self, multi: bool, val: &Value) -> Value { - let selected = self.compiled.select(&val).ok().unwrap_or_else(Vector::new); - if multi { - Value::Array(selected.into_iter().cloned().collect()) - } else { - selected.get(0).map(|v| (*v).clone()).unwrap_or(Value::Null) - } - } -} -impl fmt::Display for ConfigSelector { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.src) - } -} -impl Serialize for ConfigSelector { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.src) - } -} -impl<'de> Deserialize<'de> for ConfigSelector { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let src: String = Deserialize::deserialize(deserializer)?; - let compiled = CompiledJsonPath::compile(&src).map_err(serde::de::Error::custom)?; - Ok(Self { src, compiled }) - } -} -impl PartialEq for ConfigSelector { - fn eq(&self, other: &ConfigSelector) -> bool { - self.src == other.src - } -} -impl Eq for ConfigSelector {} -impl PartialOrd for ConfigSelector { - fn partial_cmp(&self, other: &Self) -> Option { - self.src.partial_cmp(&other.src) - } -} -impl Ord for ConfigSelector { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.src.cmp(&other.src) - } -} -impl Hash for ConfigSelector { - fn hash(&self, state: &mut H) { - self.src.hash(state) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct TorKeyPointer { - package_id: PackageId, - interface: InterfaceId, -} -impl TorKeyPointer { - async fn deref( - &self, - source_package: &PackageId, - secrets: &PgPool, - ) -> Result { - if &self.package_id != source_package { - return Err(ConfigurationError::PermissionDenied( - ValueSpecPointer::Package(PackagePointerSpec::TorKey(self.clone())), - )); - } - let key = Key::for_interface( - secrets - .acquire() - .await - .map_err(|e| ConfigurationError::SystemError(e.into()))? - .as_mut(), - Some((self.package_id.clone(), self.interface.clone())), - ) - .await - .map_err(ConfigurationError::SystemError)?; - Ok(Value::String(Arc::new(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - &key.tor_key().as_bytes(), - )))) - } -} -impl fmt::Display for TorKeyPointer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: tor-key: {}", self.package_id, self.interface) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "target")] -pub enum SystemPointerSpec {} -impl fmt::Display for SystemPointerSpec { - fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { - // write!(f, "SYSTEM: {}", match *self {}) - Ok(()) - } -} -impl SystemPointerSpec { - async fn deref(&self, _ctx: &RpcContext) -> Result { - #[allow(unreachable_code)] - Ok(match *self {}) - } -} -impl Defaultable for SystemPointerSpec { - type Error = ConfigurationError; - fn gen( - &self, - _rng: &mut R, - _timeout: &Option, - ) -> Result { - Ok(Value::Null) - } -} -#[async_trait] -impl ValueSpec for SystemPointerSpec { - fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { - Ok(()) - } - fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { - Ok(()) - } - async fn update( - &self, - ctx: &RpcContext, - _manifest: &Manifest, - _config_overrides: &BTreeMap, - value: &mut Value, - ) -> Result<(), ConfigurationError> { - *value = self.deref(ctx).await?; - Ok(()) - } - fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { - let mut pointers = BTreeSet::new(); - pointers.insert(ValueSpecPointer::System(self.clone())); - #[allow(unreachable_code)] - Ok(pointers) - } - fn requires(&self, _id: &PackageId, _value: &Value) -> bool { - false - } - fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { - false - } -} - -#[test] -fn invalid_regex_produces_error() { - assert!( - serde_yaml::from_reader::<_, ConfigSpec>(std::io::Cursor::new(include_bytes!( - "../../test/config-spec/lnd-invalid-regex.yaml" - ))) - .is_err() - ) -} - -#[test] -fn missing_pattern_description_produces_error() { - assert!( - serde_yaml::from_reader::<_, ConfigSpec>(std::io::Cursor::new(include_bytes!( - "../../test/config-spec/lnd-missing-pattern-description.yaml" - ))) - .is_err() - ) -} - -#[test] -fn missing_pattern_produces_error() { - assert!( - serde_yaml::from_reader::<_, ConfigSpec>(std::io::Cursor::new(include_bytes!( - "../../test/config-spec/lnd-missing-pattern.yaml" - ))) - .is_err() - ) -} - -#[test] -fn regex_control() { - let spec = serde_yaml::from_reader::<_, ConfigSpec>(std::io::Cursor::new(include_bytes!( - "../../test/config-spec/lnd-correct.yaml" - ))) - .unwrap(); - println!("{}", serde_json::to_string_pretty(&spec).unwrap()); -} diff --git a/core/startos/src/config/util.rs b/core/startos/src/config/util.rs deleted file mode 100644 index 359c24476..000000000 --- a/core/startos/src/config/util.rs +++ /dev/null @@ -1,406 +0,0 @@ -use std::borrow::Cow; -use std::ops::{Bound, RangeBounds, RangeInclusive}; - -use patch_db::Value; -use rand::distributions::Distribution; -use rand::Rng; - -use super::Config; - -pub const STATIC_NULL: Value = Value::Null; - -#[derive(Clone, Debug)] -pub struct CharSet(pub Vec<(RangeInclusive, usize)>, usize); -impl CharSet { - pub fn contains(&self, c: &char) -> bool { - self.0.iter().any(|r| r.0.contains(c)) - } - pub fn gen(&self, rng: &mut R) -> char { - let mut idx = rng.gen_range(0..self.1); - for r in &self.0 { - if idx < r.1 { - return std::convert::TryFrom::try_from( - rand::distributions::Uniform::new_inclusive( - u32::from(*r.0.start()), - u32::from(*r.0.end()), - ) - .sample(rng), - ) - .unwrap(); - } else { - idx -= r.1; - } - } - unreachable!() - } -} -impl Default for CharSet { - fn default() -> Self { - CharSet(vec![('!'..='~', 94)], 94) - } -} -impl<'de> serde::de::Deserialize<'de> for CharSet { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let mut res = Vec::new(); - let mut len = 0; - let mut a: Option = None; - let mut b: Option = None; - let mut in_range = false; - for c in s.chars() { - match c { - ',' => match (a, b, in_range) { - (Some(start), Some(end), _) => { - if !end.is_ascii() { - return Err(serde::de::Error::custom("Invalid Character")); - } - if start >= end { - return Err(serde::de::Error::custom("Invalid Bounds")); - } - let l = u32::from(end) - u32::from(start) + 1; - res.push((start..=end, l as usize)); - len += l as usize; - a = None; - b = None; - in_range = false; - } - (Some(start), None, false) => { - len += 1; - res.push((start..=start, 1)); - a = None; - } - (Some(_), None, true) => { - b = Some(','); - } - (None, None, false) => { - a = Some(','); - } - _ => { - return Err(serde::de::Error::custom("Syntax Error")); - } - }, - '-' => { - if a.is_none() { - a = Some('-'); - } else if !in_range { - in_range = true; - } else if b.is_none() { - b = Some('-') - } else { - return Err(serde::de::Error::custom("Syntax Error")); - } - } - _ => { - if a.is_none() { - a = Some(c); - } else if in_range && b.is_none() { - b = Some(c); - } else { - return Err(serde::de::Error::custom("Syntax Error")); - } - } - } - } - match (a, b) { - (Some(start), Some(end)) => { - if !end.is_ascii() { - return Err(serde::de::Error::custom("Invalid Character")); - } - if start >= end { - return Err(serde::de::Error::custom("Invalid Bounds")); - } - let l = u32::from(end) - u32::from(start) + 1; - res.push((start..=end, l as usize)); - len += l as usize; - } - (Some(c), None) => { - len += 1; - res.push((c..=c, 1)); - } - _ => (), - } - - Ok(CharSet(res, len)) - } -} -impl serde::ser::Serialize for CharSet { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - <&str>::serialize( - &self - .0 - .iter() - .map(|r| match r.1 { - 1 => format!("{}", r.0.start()), - _ => format!("{}-{}", r.0.start(), r.0.end()), - }) - .collect::>() - .join(",") - .as_str(), - serializer, - ) - } -} - -pub trait MergeWith { - fn merge_with(&mut self, other: &serde_json::Value); -} - -impl MergeWith for serde_json::Value { - fn merge_with(&mut self, other: &serde_json::Value) { - use serde_json::Value::Object; - if let (Object(orig), Object(ref other)) = (self, other) { - for (key, val) in other.into_iter() { - match (orig.get_mut(key), val) { - (Some(new_orig @ Object(_)), other @ Object(_)) => { - new_orig.merge_with(other); - } - (None, _) => { - orig.insert(key.clone(), val.clone()); - } - _ => (), - } - } - } - } -} - -#[test] -fn merge_with_tests() { - use serde_json::json; - - let mut a = json!( - {"a": 1, "c": {"d": "123"}, "i": [1,2,3], "j": {}, "k":[1,2,3], "l": "test"} - ); - a.merge_with( - &json!({"a":"a", "b": "b", "c":{"d":"d", "e":"e"}, "f":{"g":"g"}, "h": [1,2,3], "i":"i", "j":[1,2,3], "k":{}}), - ); - assert_eq!( - a, - json!({"a": 1, "c": {"d": "123", "e":"e"}, "b":"b", "f": {"g":"g"}, "h":[1,2,3], "i":[1,2,3], "j": {}, "k":[1,2,3], "l": "test"}) - ) -} -pub mod serde_regex { - use regex::Regex; - use serde::*; - - pub fn serialize(regex: &Regex, serializer: S) -> Result - where - S: Serializer, - { - <&str>::serialize(®ex.as_str(), serializer) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Regex::new(&s).map_err(|e| de::Error::custom(e)) - } -} - -#[derive(Clone, Debug)] -pub struct NumRange( - pub (Bound, Bound), -); -impl std::ops::Deref for NumRange -where - T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, -{ - type Target = (Bound, Bound); - - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl<'de, T> serde::de::Deserialize<'de> for NumRange -where - T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, - ::Err: std::fmt::Display, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let mut split = s.split(","); - let start = split - .next() - .map(|s| match s.get(..1) { - Some("(") => match s.get(1..2) { - Some("*") => Ok(Bound::Unbounded), - _ => s[1..] - .trim() - .parse() - .map(Bound::Excluded) - .map_err(|e| serde::de::Error::custom(e)), - }, - Some("[") => s[1..] - .trim() - .parse() - .map(Bound::Included) - .map_err(|e| serde::de::Error::custom(e)), - _ => Err(serde::de::Error::custom(format!( - "Could not parse left bound: {}", - s - ))), - }) - .transpose()? - .unwrap(); - let end = split - .next() - .map(|s| match s.get(s.len() - 1..) { - Some(")") => match s.get(s.len() - 2..s.len() - 1) { - Some("*") => Ok(Bound::Unbounded), - _ => s[..s.len() - 1] - .trim() - .parse() - .map(Bound::Excluded) - .map_err(|e| serde::de::Error::custom(e)), - }, - Some("]") => s[..s.len() - 1] - .trim() - .parse() - .map(Bound::Included) - .map_err(|e| serde::de::Error::custom(e)), - _ => Err(serde::de::Error::custom(format!( - "Could not parse right bound: {}", - s - ))), - }) - .transpose()? - .unwrap_or(Bound::Unbounded); - - Ok(NumRange((start, end))) - } -} -impl std::fmt::Display for NumRange -where - T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.start_bound() { - Bound::Excluded(n) => write!(f, "({},", n)?, - Bound::Included(n) => write!(f, "[{},", n)?, - Bound::Unbounded => write!(f, "(*,")?, - }; - match self.end_bound() { - Bound::Excluded(n) => write!(f, "{})", n), - Bound::Included(n) => write!(f, "{}]", n), - Bound::Unbounded => write!(f, "*)"), - } - } -} -impl serde::ser::Serialize for NumRange -where - T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - <&str>::serialize(&format!("{}", self).as_str(), serializer) - } -} - -#[derive(Clone, Debug)] -pub enum UniqueBy { - Any(Vec), - All(Vec), - Exactly(String), - NotUnique, -} -impl UniqueBy { - pub fn eq(&self, lhs: &Config, rhs: &Config) -> bool { - match self { - UniqueBy::Any(any) => any.iter().any(|u| u.eq(lhs, rhs)), - UniqueBy::All(all) => all.iter().all(|u| u.eq(lhs, rhs)), - UniqueBy::Exactly(key) => lhs.get(&**key) == rhs.get(&**key), - UniqueBy::NotUnique => false, - } - } -} -impl Default for UniqueBy { - fn default() -> Self { - UniqueBy::NotUnique - } -} -impl<'de> serde::de::Deserialize<'de> for UniqueBy { - fn deserialize>(deserializer: D) -> Result { - struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = UniqueBy; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a key, an \"any\" object, or an \"all\" object") - } - fn visit_str(self, v: &str) -> Result { - Ok(UniqueBy::Exactly(v.to_owned())) - } - fn visit_string(self, v: String) -> Result { - Ok(UniqueBy::Exactly(v)) - } - fn visit_map>( - self, - mut map: A, - ) -> Result { - let mut variant = None; - while let Some(key) = map.next_key::>()? { - match key.as_ref() { - "any" => { - return Ok(UniqueBy::Any(map.next_value()?)); - } - "all" => { - return Ok(UniqueBy::All(map.next_value()?)); - } - _ => { - variant = Some(key); - } - } - } - Err(serde::de::Error::unknown_variant( - variant.unwrap_or_default().as_ref(), - &["any", "all"], - )) - } - fn visit_unit(self) -> Result { - Ok(UniqueBy::NotUnique) - } - fn visit_none(self) -> Result { - Ok(UniqueBy::NotUnique) - } - } - deserializer.deserialize_any(Visitor) - } -} - -impl serde::ser::Serialize for UniqueBy { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - use serde::ser::SerializeMap; - - match self { - UniqueBy::Any(any) => { - let mut map = serializer.serialize_map(Some(1))?; - map.serialize_key("any")?; - map.serialize_value(any)?; - map.end() - } - UniqueBy::All(all) => { - let mut map = serializer.serialize_map(Some(1))?; - map.serialize_key("all")?; - map.serialize_value(all)?; - map.end() - } - UniqueBy::Exactly(key) => serializer.serialize_str(key), - UniqueBy::NotUnique => serializer.serialize_unit(), - } - } -} diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index 020b73459..0eca1d2c2 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -1,43 +1,39 @@ use std::fs::File; use std::io::BufReader; -use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use std::sync::Arc; -use clap::ArgMatches; -use color_eyre::eyre::eyre; use cookie_store::{CookieStore, RawCookie}; use josekit::jwk::Jwk; +use once_cell::sync::OnceCell; use reqwest::Proxy; use reqwest_cookie_store::CookieStoreMutex; use rpc_toolkit::reqwest::{Client, Url}; -use rpc_toolkit::url::Host; -use rpc_toolkit::Context; -use serde::Deserialize; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{call_remote_http, CallRemote, Context, Empty}; +use tokio::net::TcpStream; +use tokio::runtime::Runtime; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tracing::instrument; use super::setup::CURRENT_SECRET; +use crate::context::config::{local_config_path, ClientConfig}; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; -use crate::util::config::{load_config_from_paths, local_config_path}; -use crate::ResultExt; - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct CliContextConfig { - pub host: Option, - #[serde(deserialize_with = "crate::util::serde::deserialize_from_str_opt")] - #[serde(default)] - pub proxy: Option, - pub cookie_path: Option, -} +use crate::prelude::*; +use crate::rpc_continuations::Guid; #[derive(Debug)] pub struct CliContextSeed { + pub runtime: OnceCell>, pub base_url: Url, pub rpc_url: Url, + pub registry_url: Option, pub client: Client, pub cookie_store: Arc, pub cookie_path: PathBuf, + pub developer_key_path: PathBuf, + pub developer_key: OnceCell, } impl Drop for CliContextSeed { fn drop(&mut self) { @@ -47,7 +43,9 @@ impl Drop for CliContextSeed { std::fs::create_dir_all(&parent_dir).unwrap(); } let mut writer = fd_lock_rs::FdLock::lock( - File::create(&tmp).unwrap(), + File::create(&tmp) + .with_ctx(|_| (ErrorKind::Filesystem, &tmp)) + .unwrap(), fd_lock_rs::LockType::Exclusive, true, ) @@ -60,51 +58,36 @@ impl Drop for CliContextSeed { } } -const DEFAULT_HOST: Host<&'static str> = Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)); -const DEFAULT_PORT: u16 = 5959; - #[derive(Debug, Clone)] pub struct CliContext(Arc); impl CliContext { /// BLOCKING #[instrument(skip_all)] - pub fn init(matches: &ArgMatches) -> Result { - let local_config_path = local_config_path(); - let base: CliContextConfig = load_config_from_paths( - matches - .values_of("config") - .into_iter() - .flatten() - .map(|p| Path::new(p)) - .chain(local_config_path.as_deref().into_iter()) - .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), - )?; - let mut url = if let Some(host) = matches.value_of("host") { - host.parse()? - } else if let Some(host) = base.host { + pub fn init(config: ClientConfig) -> Result { + let mut url = if let Some(host) = config.host { host } else { "http://localhost".parse()? }; - let proxy = if let Some(proxy) = matches.value_of("proxy") { - Some(proxy.parse()?) - } else { - base.proxy - }; - let cookie_path = base.cookie_path.unwrap_or_else(|| { - local_config_path + let registry = config.registry.clone(); + + let cookie_path = config.cookie_path.unwrap_or_else(|| { + local_config_path() .as_deref() - .unwrap_or_else(|| Path::new(crate::util::config::CONFIG_PATH)) + .unwrap_or_else(|| Path::new(super::config::CONFIG_PATH)) .parent() .unwrap_or(Path::new("/")) .join(".cookies.json") }); let cookie_store = Arc::new(CookieStoreMutex::new({ let mut store = if cookie_path.exists() { - CookieStore::load_json(BufReader::new(File::open(&cookie_path)?)) - .map_err(|e| eyre!("{}", e)) - .with_kind(crate::ErrorKind::Deserialization)? + CookieStore::load_json(BufReader::new( + File::open(&cookie_path) + .with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?, + )) + .map_err(|e| eyre!("{}", e)) + .with_kind(crate::ErrorKind::Deserialization)? } else { CookieStore::default() }; @@ -120,6 +103,7 @@ impl CliContext { })); Ok(CliContext(Arc::new(CliContextSeed { + runtime: OnceCell::new(), base_url: url.clone(), rpc_url: { url.path_segments_mut() @@ -129,9 +113,20 @@ impl CliContext { .push("v1"); url }, + registry_url: registry + .map(|mut registry| { + registry + .path_segments_mut() + .map_err(|_| eyre!("Url cannot be base")) + .with_kind(crate::ErrorKind::ParseUrl)? + .push("rpc") + .push("v0"); + Ok::<_, Error>(registry) + }) + .transpose()?, client: { let mut builder = Client::builder().cookie_provider(cookie_store.clone()); - if let Some(proxy) = proxy { + if let Some(proxy) = config.proxy { builder = builder.proxy(Proxy::all(proxy).with_kind(crate::ErrorKind::ParseUrl)?) } @@ -139,8 +134,113 @@ impl CliContext { }, cookie_store, cookie_path, + developer_key_path: config.developer_key_path.unwrap_or_else(|| { + local_config_path() + .as_deref() + .unwrap_or_else(|| Path::new(super::config::CONFIG_PATH)) + .parent() + .unwrap_or(Path::new("/")) + .join("developer.key.pem") + }), + developer_key: OnceCell::new(), }))) } + + /// BLOCKING + #[instrument(skip_all)] + pub fn developer_key(&self) -> Result<&ed25519_dalek::SigningKey, Error> { + self.developer_key.get_or_try_init(|| { + if !self.developer_key_path.exists() { + return Err(Error::new(eyre!("Developer Key does not exist! Please run `start-cli init` before running this command."), crate::ErrorKind::Uninitialized)); + } + let pair = ::from_pkcs8_pem( + &std::fs::read_to_string(&self.developer_key_path)?, + ) + .with_kind(crate::ErrorKind::Pem)?; + let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| { + Error::new( + eyre!("pkcs8 key is of incorrect length"), + ErrorKind::OpenSsl, + ) + })?; + Ok(secret.into()) + }) + } + + pub async fn ws_continuation( + &self, + guid: Guid, + ) -> Result>, Error> { + let mut url = self.base_url.clone(); + let ws_scheme = match url.scheme() { + "https" => "wss", + "http" => "ws", + _ => { + return Err(Error::new( + eyre!("Cannot parse scheme from base URL"), + crate::ErrorKind::ParseUrl, + ) + .into()) + } + }; + url.set_scheme(ws_scheme) + .map_err(|_| Error::new(eyre!("Cannot set URL scheme"), crate::ErrorKind::ParseUrl))?; + url.path_segments_mut() + .map_err(|_| eyre!("Url cannot be base")) + .with_kind(crate::ErrorKind::ParseUrl)? + .push("ws") + .push("rpc") + .push(guid.as_ref()); + let (stream, _) = + // base_url is "http://127.0.0.1/", with a trailing slash, so we don't put a leading slash in this path: + tokio_tungstenite::connect_async(url).await.with_kind(ErrorKind::Network)?; + Ok(stream) + } + + pub async fn rest_continuation( + &self, + guid: Guid, + body: reqwest::Body, + headers: reqwest::header::HeaderMap, + ) -> Result { + let mut url = self.base_url.clone(); + url.path_segments_mut() + .map_err(|_| eyre!("Url cannot be base")) + .with_kind(crate::ErrorKind::ParseUrl)? + .push("rest") + .push("rpc") + .push(guid.as_ref()); + self.client + .post(url) + .headers(headers) + .body(body) + .send() + .await + .with_kind(ErrorKind::Network) + } + + pub async fn call_remote( + &self, + method: &str, + params: Value, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, Empty {}) + .await + } + pub async fn call_remote_with( + &self, + method: &str, + params: Value, + extra: T, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, extra).await + } } impl AsRef for CliContext { fn as_ref(&self) -> &Jwk { @@ -154,32 +254,55 @@ impl std::ops::Deref for CliContext { } } impl Context for CliContext { - fn protocol(&self) -> &str { - self.0.base_url.scheme() + fn runtime(&self) -> Option> { + Some( + self.runtime + .get_or_init(|| { + Arc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ) + }) + .clone(), + ) } - fn host(&self) -> Host<&str> { - self.0.base_url.host().unwrap_or(DEFAULT_HOST) +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } - fn port(&self) -> u16 { - self.0.base_url.port().unwrap_or(DEFAULT_PORT) +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } - fn path(&self) -> &str { - self.0.rpc_url.path() +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } - fn url(&self) -> Url { - self.0.rpc_url.clone() +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } - fn client(&self) -> &Client { - &self.0.client +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } } -/// When we had an empty proxy the system wasn't working like it used to, which allowed empty proxy + #[test] -fn test_cli_proxy_empty() { - serde_yaml::from_str::( - " - bind_rpc: - ", - ) - .unwrap(); +fn test() { + let ctx = CliContext::init(ClientConfig::default()).unwrap(); + ctx.runtime().unwrap().block_on(async { + reqwest::Client::new() + .get("http://example.com") + .send() + .await + .unwrap(); + }); } diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs new file mode 100644 index 000000000..4a0925d81 --- /dev/null +++ b/core/startos/src/context/config.rs @@ -0,0 +1,163 @@ +use std::fs::File; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; + +use clap::Parser; +use reqwest::Url; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgConnectOptions; +use sqlx::PgPool; + +use crate::disk::OsPartitionInfo; +use crate::init::init_postgres; +use crate::prelude::*; +use crate::util::serde::IoFormat; +use crate::MAIN_DATA; + +pub const DEVICE_CONFIG_PATH: &str = "/media/startos/config/config.yaml"; // "/media/startos/config/config.yaml"; +pub const CONFIG_PATH: &str = "/etc/startos/config.yaml"; +pub const CONFIG_PATH_LOCAL: &str = ".startos/config.yaml"; + +pub fn local_config_path() -> Option { + if let Ok(home) = std::env::var("HOME") { + Some(Path::new(&home).join(CONFIG_PATH_LOCAL)) + } else { + None + } +} + +pub trait ContextConfig: DeserializeOwned + Default { + fn next(&mut self) -> Option; + fn merge_with(&mut self, other: Self); + fn from_path(path: impl AsRef) -> Result { + let format: IoFormat = path + .as_ref() + .extension() + .and_then(|s| s.to_str()) + .map(|f| f.parse()) + .transpose()? + .unwrap_or_default(); + format.from_reader( + File::open(path.as_ref()) + .with_ctx(|_| (ErrorKind::Filesystem, path.as_ref().display()))?, + ) + } + fn load_path_rec(&mut self, path: Option>) -> Result<(), Error> { + if let Some(path) = path.filter(|p| p.as_ref().exists()) { + let mut other = Self::from_path(path)?; + let path = other.next(); + self.merge_with(other); + self.load_path_rec(path)?; + } + Ok(()) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct ClientConfig { + #[arg(short = 'c', long = "config")] + pub config: Option, + #[arg(short = 'h', long = "host")] + pub host: Option, + #[arg(short = 'r', long = "registry")] + pub registry: Option, + #[arg(short = 'p', long = "proxy")] + pub proxy: Option, + #[arg(long = "cookie-path")] + pub cookie_path: Option, + #[arg(long = "developer-key-path")] + pub developer_key_path: Option, +} +impl ContextConfig for ClientConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.host = self.host.take().or(other.host); + self.registry = self.registry.take().or(other.registry); + self.proxy = self.proxy.take().or(other.proxy); + self.cookie_path = self.cookie_path.take().or(other.cookie_path); + self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path); + } +} +impl ClientConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(local_config_path())?; + self.load_path_rec(Some(CONFIG_PATH))?; + Ok(self) + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct ServerConfig { + #[arg(short, long)] + pub config: Option, + #[arg(long)] + pub ethernet_interface: Option, + #[arg(skip)] + pub os_partitions: Option, + #[arg(long)] + pub tor_control: Option, + #[arg(long)] + pub tor_socks: Option, + #[arg(long)] + pub revision_cache_size: Option, + #[arg(long)] + pub disable_encryption: Option, + #[arg(long)] + pub multi_arch_s9pks: Option, +} +impl ContextConfig for ServerConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.ethernet_interface = self.ethernet_interface.take().or(other.ethernet_interface); + self.os_partitions = self.os_partitions.take().or(other.os_partitions); + self.tor_control = self.tor_control.take().or(other.tor_control); + self.tor_socks = self.tor_socks.take().or(other.tor_socks); + self.revision_cache_size = self + .revision_cache_size + .take() + .or(other.revision_cache_size); + self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); + self.multi_arch_s9pks = self.multi_arch_s9pks.take().or(other.multi_arch_s9pks); + } +} + +impl ServerConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(Some(DEVICE_CONFIG_PATH))?; + self.load_path_rec(Some(CONFIG_PATH))?; + Ok(self) + } + pub async fn db(&self) -> Result { + let db_path = Path::new(MAIN_DATA).join("embassy.db"); + let db = PatchDb::open(&db_path) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?; + + Ok(db) + } + #[instrument(skip_all)] + pub async fn secret_store(&self) -> Result { + init_postgres("/media/startos/data").await?; + let secret_store = + PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root")) + .await?; + sqlx::migrate!() + .run(&secret_store) + .await + .with_kind(crate::ErrorKind::Database)?; + Ok(secret_store) + } +} diff --git a/core/startos/src/context/diagnostic.rs b/core/startos/src/context/diagnostic.rs index 151948d7c..6acb21e30 100644 --- a/core/startos/src/context/diagnostic.rs +++ b/core/startos/src/context/diagnostic.rs @@ -1,79 +1,50 @@ use std::ops::Deref; -use std::path::{Path, PathBuf}; use std::sync::Arc; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::Context; -use serde::Deserialize; use tokio::sync::broadcast::Sender; use tracing::instrument; +use crate::context::config::ServerConfig; +use crate::rpc_continuations::RpcContinuations; use crate::shutdown::Shutdown; -use crate::util::config::load_config_from_paths; use crate::Error; -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct DiagnosticContextConfig { - pub datadir: Option, -} -impl DiagnosticContextConfig { - #[instrument(skip_all)] - pub async fn load + Send + 'static>(path: Option

) -> Result { - tokio::task::spawn_blocking(move || { - load_config_from_paths( - path.as_ref() - .into_iter() - .map(|p| p.as_ref()) - .chain(std::iter::once(Path::new( - crate::util::config::DEVICE_CONFIG_PATH, - ))) - .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), - ) - }) - .await - .unwrap() - } - - pub fn datadir(&self) -> &Path { - self.datadir - .as_deref() - .unwrap_or_else(|| Path::new("/embassy-data")) - } -} - pub struct DiagnosticContextSeed { - pub datadir: PathBuf, - pub shutdown: Sender>, + pub shutdown: Sender, pub error: Arc, pub disk_guid: Option>, + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] pub struct DiagnosticContext(Arc); impl DiagnosticContext { #[instrument(skip_all)] - pub async fn init + Send + 'static>( - path: Option

, + pub fn init( + _config: &ServerConfig, disk_guid: Option>, error: Error, ) -> Result { tracing::error!("Error: {}: Starting diagnostic UI", error); tracing::debug!("{:?}", error); - let cfg = DiagnosticContextConfig::load(path).await?; - let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(DiagnosticContextSeed { - datadir: cfg.datadir().to_owned(), shutdown, disk_guid, error: Arc::new(error.into()), + rpc_continuations: RpcContinuations::new(), }))) } } - +impl AsRef for DiagnosticContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} impl Context for DiagnosticContext {} impl Deref for DiagnosticContext { type Target = DiagnosticContextSeed; diff --git a/core/startos/src/context/init.rs b/core/startos/src/context/init.rs new file mode 100644 index 000000000..566457a9c --- /dev/null +++ b/core/startos/src/context/init.rs @@ -0,0 +1,50 @@ +use std::ops::Deref; +use std::sync::Arc; + +use rpc_toolkit::Context; +use tokio::sync::broadcast::Sender; +use tokio::sync::watch; +use tracing::instrument; + +use crate::context::config::ServerConfig; +use crate::progress::FullProgressTracker; +use crate::rpc_continuations::RpcContinuations; +use crate::Error; + +pub struct InitContextSeed { + pub config: ServerConfig, + pub error: watch::Sender>, + pub progress: FullProgressTracker, + pub shutdown: Sender<()>, + pub rpc_continuations: RpcContinuations, +} + +#[derive(Clone)] +pub struct InitContext(Arc); +impl InitContext { + #[instrument(skip_all)] + pub async fn init(cfg: &ServerConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + Ok(Self(Arc::new(InitContextSeed { + config: cfg.clone(), + error: watch::channel(None).0, + progress: FullProgressTracker::new(), + shutdown, + rpc_continuations: RpcContinuations::new(), + }))) + } +} + +impl AsRef for InitContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for InitContext {} +impl Deref for InitContext { + type Target = InitContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/core/startos/src/context/install.rs b/core/startos/src/context/install.rs index 87484b7e5..c0c564b34 100644 --- a/core/startos/src/context/install.rs +++ b/core/startos/src/context/install.rs @@ -1,54 +1,40 @@ use std::ops::Deref; -use std::path::Path; use std::sync::Arc; use rpc_toolkit::Context; -use serde::Deserialize; use tokio::sync::broadcast::Sender; use tracing::instrument; use crate::net::utils::find_eth_iface; -use crate::util::config::load_config_from_paths; +use crate::rpc_continuations::RpcContinuations; use crate::Error; -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct InstallContextConfig {} -impl InstallContextConfig { - #[instrument(skip_all)] - pub async fn load + Send + 'static>(path: Option

) -> Result { - tokio::task::spawn_blocking(move || { - load_config_from_paths( - path.as_ref() - .into_iter() - .map(|p| p.as_ref()) - .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), - ) - }) - .await - .unwrap() - } -} - pub struct InstallContextSeed { pub ethernet_interface: String, pub shutdown: Sender<()>, + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] pub struct InstallContext(Arc); impl InstallContext { #[instrument(skip_all)] - pub async fn init + Send + 'static>(path: Option

) -> Result { - let _cfg = InstallContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?; + pub async fn init() -> Result { let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(InstallContextSeed { ethernet_interface: find_eth_iface().await?, shutdown, + rpc_continuations: RpcContinuations::new(), }))) } } +impl AsRef for InstallContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + impl Context for InstallContext {} impl Deref for InstallContext { type Target = InstallContextSeed; diff --git a/core/startos/src/context/mod.rs b/core/startos/src/context/mod.rs index c4e8e7757..efe261b0c 100644 --- a/core/startos/src/context/mod.rs +++ b/core/startos/src/context/mod.rs @@ -1,44 +1,14 @@ pub mod cli; +pub mod config; pub mod diagnostic; +pub mod init; pub mod install; pub mod rpc; -pub mod sdk; pub mod setup; pub use cli::CliContext; pub use diagnostic::DiagnosticContext; +pub use init::InitContext; pub use install::InstallContext; pub use rpc::RpcContext; -pub use sdk::SdkContext; pub use setup::SetupContext; - -impl From for () { - fn from(_: CliContext) -> Self { - () - } -} -impl From for () { - fn from(_: DiagnosticContext) -> Self { - () - } -} -impl From for () { - fn from(_: RpcContext) -> Self { - () - } -} -impl From for () { - fn from(_: SdkContext) -> Self { - () - } -} -impl From for () { - fn from(_: SetupContext) -> Self { - () - } -} -impl From for () { - fn from(_: InstallContext) -> Self { - () - } -} diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 5358a59ba..dfaa02b01 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -1,131 +1,79 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::ops::Deref; -use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use helpers::to_tmp_path; +use chrono::{TimeDelta, Utc}; +use helpers::NonDetachingJoinHandle; +use imbl::OrdMap; +use imbl_value::InternedString; +use itertools::Itertools; use josekit::jwk::Jwk; -use patch_db::json_ptr::JsonPointer; -use patch_db::PatchDb; -use reqwest::{Client, Proxy, Url}; -use rpc_toolkit::Context; -use serde::Deserialize; -use sqlx::postgres::PgConnectOptions; -use sqlx::PgPool; -use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; +use models::{ActionId, PackageId}; +use reqwest::{Client, Proxy}; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{CallRemote, Context, Empty}; +use tokio::sync::{broadcast, watch, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; -use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation}; -use crate::db::model::{CurrentDependents, Database, PackageDataEntryMatchModelRef}; -use crate::db::prelude::PatchDbExt; -use crate::dependencies::compute_dependency_config_errs; +use crate::auth::Sessions; +use crate::context::config::ServerConfig; +use crate::db::model::Database; use crate::disk::OsPartitionInfo; -use crate::init::{check_time_is_synchronized, init_postgres}; -use crate::install::cleanup::{cleanup_failed, uninstall}; -use crate::manager::ManagerMap; -use crate::middleware::auth::HashSessionToken; -use crate::net::net_controller::NetController; -use crate::net::ssl::{root_ca_start_time, SslManager}; +use crate::init::{check_time_is_synchronized, InitResult}; +use crate::lxc::{ContainerId, LxcContainer, LxcManager}; +use crate::net::net_controller::{NetController, NetService}; +use crate::net::utils::{find_eth_iface, find_wifi_iface}; +use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter}; use crate::net::wifi::WpaCli; -use crate::notifications::NotificationManager; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle}; +use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations}; +use crate::service::action::update_requested_actions; +use crate::service::effects::callbacks::ServiceCallbacks; +use crate::service::ServiceMap; use crate::shutdown::Shutdown; -use crate::status::MainStatus; -use crate::system::get_mem_info; -use crate::util::config::load_config_from_paths; -use crate::util::lshw::{lshw, LshwDevice}; -use crate::{Error, ErrorKind, ResultExt}; - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct RpcContextConfig { - pub wifi_interface: Option, - pub ethernet_interface: String, - pub os_partitions: OsPartitionInfo, - pub migration_batch_rows: Option, - pub migration_prefetch_rows: Option, - pub bind_rpc: Option, - pub tor_control: Option, - pub tor_socks: Option, - pub dns_bind: Option>, - pub revision_cache_size: Option, - pub datadir: Option, - pub log_server: Option, -} -impl RpcContextConfig { - pub async fn load + Send + 'static>(path: Option

) -> Result { - tokio::task::spawn_blocking(move || { - load_config_from_paths( - path.as_ref() - .into_iter() - .map(|p| p.as_ref()) - .chain(std::iter::once(Path::new( - crate::util::config::DEVICE_CONFIG_PATH, - ))) - .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), - ) - }) - .await - .unwrap() - } - pub fn datadir(&self) -> &Path { - self.datadir - .as_deref() - .unwrap_or_else(|| Path::new("/embassy-data")) - } - pub async fn db(&self, account: &AccountInfo) -> Result { - let db_path = self.datadir().join("main").join("embassy.db"); - let db = PatchDb::open(&db_path) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?; - if !db.exists(&::default()).await { - db.put(&::default(), &Database::init(account)) - .await?; - } - Ok(db) - } - #[instrument(skip_all)] - pub async fn secret_store(&self) -> Result { - init_postgres(self.datadir()).await?; - let secret_store = - PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root")) - .await?; - sqlx::migrate!() - .run(&secret_store) - .await - .with_kind(crate::ErrorKind::Database)?; - Ok(secret_store) - } -} +use crate::util::lshw::LshwDevice; +use crate::util::sync::SyncMutex; pub struct RpcContextSeed { is_closed: AtomicBool, pub os_partitions: OsPartitionInfo, pub wifi_interface: Option, pub ethernet_interface: String, - pub datadir: PathBuf, pub disk_guid: Arc, - pub db: PatchDb, - pub secret_store: PgPool, + pub ephemeral_sessions: SyncMutex, + pub db: TypedPatchDb, + pub sync_db: watch::Sender, pub account: RwLock, pub net_controller: Arc, - pub managers: ManagerMap, + pub os_net_service: NetService, + pub s9pk_arch: Option<&'static str>, + pub services: ServiceMap, pub metrics_cache: RwLock>, pub shutdown: broadcast::Sender>, pub tor_socks: SocketAddr, - pub notification_manager: NotificationManager, - pub open_authed_websockets: Mutex>>>, - pub rpc_stream_continuations: Mutex>, + pub lxc_manager: Arc, + pub open_authed_continuations: OpenAuthedContinuations>, + pub rpc_continuations: RpcContinuations, + pub callbacks: ServiceCallbacks, pub wifi_manager: Option>>, pub current_secret: Arc, pub client: Client, - pub hardware: Hardware, pub start_time: Instant, + pub crons: SyncMutex>>, + // #[cfg(feature = "dev")] + pub dev: Dev, +} + +pub struct Dev { + pub lxc: Mutex>, } pub struct Hardware { @@ -133,81 +81,171 @@ pub struct Hardware { pub ram: u64, } +pub struct InitRpcContextPhases { + load_db: PhaseProgressTrackerHandle, + init_net_ctrl: PhaseProgressTrackerHandle, + cleanup_init: CleanupInitPhases, + run_migrations: PhaseProgressTrackerHandle, +} +impl InitRpcContextPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + load_db: handle.add_phase("Loading database".into(), Some(5)), + init_net_ctrl: handle.add_phase("Initializing network".into(), Some(1)), + cleanup_init: CleanupInitPhases::new(handle), + run_migrations: handle.add_phase("Running migrations".into(), Some(10)), + } + } +} + +pub struct CleanupInitPhases { + cleanup_sessions: PhaseProgressTrackerHandle, + init_services: PhaseProgressTrackerHandle, + check_requested_actions: PhaseProgressTrackerHandle, +} +impl CleanupInitPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + cleanup_sessions: handle.add_phase("Cleaning up sessions".into(), Some(1)), + init_services: handle.add_phase("Initializing services".into(), Some(10)), + check_requested_actions: handle.add_phase("Checking action requests".into(), Some(1)), + } + } +} + #[derive(Clone)] pub struct RpcContext(Arc); impl RpcContext { #[instrument(skip_all)] - pub async fn init + Send + Sync + 'static>( - cfg_path: Option

, + pub async fn init( + webserver: &WebServerAcceptorSetter, + config: &ServerConfig, disk_guid: Arc, + init_result: Option, + InitRpcContextPhases { + mut load_db, + mut init_net_ctrl, + cleanup_init, + run_migrations, + }: InitRpcContextPhases, ) -> Result { - let base = RpcContextConfig::load(cfg_path).await?; - tracing::info!("Loaded Config"); - let tor_proxy = base.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new( + let tor_proxy = config.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::new(127, 0, 0, 1), 9050, ))); let (shutdown, _) = tokio::sync::broadcast::channel(1); - let secret_store = base.secret_store().await?; - tracing::info!("Opened Pg DB"); - let account = AccountInfo::load(&secret_store).await?; - let db = base.db(&account).await?; + + load_db.start(); + let db = if let Some(InitResult { net_ctrl, .. }) = &init_result { + net_ctrl.db.clone() + } else { + TypedPatchDb::::load(config.db().await?).await? + }; + let peek = db.peek().await; + let account = AccountInfo::load(&peek)?; + load_db.complete(); tracing::info!("Opened PatchDB"); - let net_controller = Arc::new( - NetController::init( - base.tor_control - .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), - tor_proxy, - base.dns_bind - .as_deref() - .unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]), - SslManager::new(&account, root_ca_start_time().await?)?, - &account.hostname, - &account.key, - ) - .await?, - ); + + init_net_ctrl.start(); + let (net_controller, os_net_service) = if let Some(InitResult { + net_ctrl, + os_net_service, + }) = init_result + { + (net_ctrl, os_net_service) + } else { + let net_ctrl = Arc::new( + NetController::init( + db.clone(), + config + .tor_control + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), + tor_proxy, + &account.hostname, + ) + .await?, + ); + webserver.try_upgrade(|a| net_ctrl.net_iface.upgrade_listener(a))?; + let os_net_service = net_ctrl.os_bindings().await?; + (net_ctrl, os_net_service) + }; + init_net_ctrl.complete(); tracing::info!("Initialized Net Controller"); - let managers = ManagerMap::default(); + + let services = ServiceMap::default(); let metrics_cache = RwLock::>::new(None); - let notification_manager = NotificationManager::new(secret_store.clone()); - tracing::info!("Initialized Notification Manager"); let tor_proxy_url = format!("socks5h://{tor_proxy}"); - let devices = lshw().await?; - let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024; - if !db.peek().await.as_server_info().as_ntp_synced().de()? { + let crons = SyncMutex::new(BTreeMap::new()); + + if !db + .peek() + .await + .as_public() + .as_server_info() + .as_ntp_synced() + .de()? + { let db = db.clone(); - tokio::spawn(async move { - while !check_time_is_synchronized().await.unwrap() { - tokio::time::sleep(Duration::from_secs(30)).await; - } - db.mutate(|v| v.as_server_info_mut().as_ntp_synced_mut().ser(&true)) - .await - .unwrap() + crons.mutate(|c| { + c.insert( + Guid::new(), + tokio::spawn(async move { + while !check_time_is_synchronized().await.unwrap() { + tokio::time::sleep(Duration::from_secs(30)).await; + } + db.mutate(|v| { + v.as_public_mut() + .as_server_info_mut() + .as_ntp_synced_mut() + .ser(&true) + }) + .await + .unwrap() + }) + .into(), + ) }); } + let wifi_interface = find_wifi_iface().await?; + let seed = Arc::new(RpcContextSeed { is_closed: AtomicBool::new(false), - datadir: base.datadir().to_path_buf(), - os_partitions: base.os_partitions, - wifi_interface: base.wifi_interface.clone(), - ethernet_interface: base.ethernet_interface, + os_partitions: config.os_partitions.clone().ok_or_else(|| { + Error::new( + eyre!("OS Partition Information Missing"), + ErrorKind::Filesystem, + ) + })?, + wifi_interface: wifi_interface.clone(), + ethernet_interface: if let Some(eth) = config.ethernet_interface.clone() { + eth + } else { + find_eth_iface().await? + }, disk_guid, + ephemeral_sessions: SyncMutex::new(Sessions::new()), + sync_db: watch::Sender::new(db.sequence().await), db, - secret_store, account: RwLock::new(account), net_controller, - managers, + os_net_service, + s9pk_arch: if config.multi_arch_s9pks.unwrap_or(false) { + None + } else { + Some(crate::ARCH) + }, + services, metrics_cache, shutdown, tor_socks: tor_proxy, - notification_manager, - open_authed_websockets: Mutex::new(BTreeMap::new()), - rpc_stream_continuations: Mutex::new(BTreeMap::new()), - wifi_manager: base - .wifi_interface + lxc_manager: Arc::new(LxcManager::new()), + open_authed_continuations: OpenAuthedContinuations::new(), + rpc_continuations: RpcContinuations::new(), + callbacks: Default::default(), + wifi_manager: wifi_interface + .clone() .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), current_secret: Arc::new( Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| { @@ -229,223 +267,188 @@ impl RpcContext { })) .build() .with_kind(crate::ErrorKind::ParseUrl)?, - hardware: Hardware { devices, ram }, start_time: Instant::now(), + crons, + // #[cfg(feature = "dev")] + dev: Dev { + lxc: Mutex::new(BTreeMap::new()), + }, }); let res = Self(seed.clone()); - res.cleanup_and_initialize().await?; + res.cleanup_and_initialize(cleanup_init).await?; tracing::info!("Cleaned up transient states"); + + crate::version::post_init(&res, run_migrations).await?; + tracing::info!("Completed migrations"); Ok(res) } #[instrument(skip_all)] pub async fn shutdown(self) -> Result<(), Error> { - self.managers.empty().await?; - self.secret_store.close().await; + self.crons.mutate(|c| std::mem::take(c)); + self.services.shutdown_all().await?; self.is_closed.store(true, Ordering::SeqCst); tracing::info!("RPC Context is shutdown"); - // TODO: shutdown http servers Ok(()) } - #[instrument(skip(self))] - pub async fn cleanup_and_initialize(&self) -> Result<(), Error> { + pub fn add_cron + Send + 'static>(&self, fut: F) -> Guid { + let guid = Guid::new(); + self.crons + .mutate(|c| c.insert(guid.clone(), tokio::spawn(fut).into())); + guid + } + + #[instrument(skip_all)] + pub async fn cleanup_and_initialize( + &self, + CleanupInitPhases { + mut cleanup_sessions, + init_services, + mut check_requested_actions, + }: CleanupInitPhases, + ) -> Result<(), Error> { + cleanup_sessions.start(); self.db - .mutate(|f| { - let mut current_dependents = f - .as_package_data() - .keys()? - .into_iter() - .map(|k| (k.clone(), BTreeMap::new())) - .collect::>(); - for (package_id, package) in f.as_package_data_mut().as_entries_mut()? { - for (k, v) in package - .as_installed_mut() - .into_iter() - .flat_map(|i| i.clone().into_current_dependencies().into_entries()) - .flatten() - { - let mut entry: BTreeMap<_, _> = - current_dependents.remove(&k).unwrap_or_default(); - entry.insert(package_id.clone(), v.de()?); - current_dependents.insert(k, entry); - } - } - for (package_id, current_dependents) in current_dependents { - if let Some(deps) = f - .as_package_data_mut() - .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_installed_mut().ok()) - .map(|i| i.as_installed_mut().as_current_dependents_mut()) - { - deps.ser(&CurrentDependents(current_dependents))?; - } else if let Some(deps) = f - .as_package_data_mut() - .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_removing_mut().ok()) - .map(|i| i.as_removing_mut().as_current_dependents_mut()) - { - deps.ser(&CurrentDependents(current_dependents))?; + .mutate(|db| { + if db.as_public().as_server_info().as_ntp_synced().de()? { + for id in db.as_private().as_sessions().keys()? { + if Utc::now() + - db.as_private() + .as_sessions() + .as_idx(&id) + .unwrap() + .de()? + .last_active + > TimeDelta::days(30) + { + db.as_private_mut().as_sessions_mut().remove(&id)?; + } } } Ok(()) }) .await?; - - let peek = self.db.peek().await; - - for (package_id, package) in peek.as_package_data().as_entries()?.into_iter() { - let action = match package.as_match() { - PackageDataEntryMatchModelRef::Installing(_) - | PackageDataEntryMatchModelRef::Restoring(_) - | PackageDataEntryMatchModelRef::Updating(_) => { - cleanup_failed(self, &package_id).await - } - PackageDataEntryMatchModelRef::Removing(_) => { - uninstall( - self, - self.secret_store.acquire().await?.as_mut(), - &package_id, - ) - .await - } - PackageDataEntryMatchModelRef::Installed(m) => { - let version = m.as_manifest().as_version().clone().de()?; - let volumes = m.as_manifest().as_volumes().de()?; - for (volume_id, volume_info) in &*volumes { - let tmp_path = to_tmp_path(volume_info.path_for( - &self.datadir, - &package_id, - &version, - volume_id, - )) - .with_kind(ErrorKind::Filesystem)?; - if tokio::fs::metadata(&tmp_path).await.is_ok() { - tokio::fs::remove_dir_all(&tmp_path).await?; + let db = self.db.clone(); + self.add_cron(async move { + loop { + tokio::time::sleep(Duration::from_secs(86400)).await; + if let Err(e) = db + .mutate(|db| { + if db.as_public().as_server_info().as_ntp_synced().de()? { + for id in db.as_private().as_sessions().keys()? { + if Utc::now() + - db.as_private() + .as_sessions() + .as_idx(&id) + .unwrap() + .de()? + .last_active + > TimeDelta::days(30) + { + db.as_private_mut().as_sessions_mut().remove(&id)?; + } + } } - } - Ok(()) + Ok(()) + }) + .await + { + tracing::error!("Error in session cleanup cron: {e}"); + tracing::debug!("{e:?}"); } - _ => continue, - }; - if let Err(e) = action { - tracing::error!("Failed to clean up package {}: {}", package_id, e); - tracing::debug!("{:?}", e); } - } - let peek = self - .db - .mutate(|v| { - for (_, pde) in v.as_package_data_mut().as_entries_mut()? { - let status = pde - .expect_as_installed_mut()? - .as_installed_mut() - .as_status_mut() - .as_main_mut(); - let running = status.clone().de()?.running(); - status.ser(&if running { - MainStatus::Starting - } else { - MainStatus::Stopped - })?; - } - Ok(v.clone()) - }) - .await?; - self.managers.init(self.clone(), peek.clone()).await?; - tracing::info!("Initialized Package Managers"); + }); + cleanup_sessions.complete(); - let mut all_dependency_config_errs = BTreeMap::new(); - for (package_id, package) in peek.as_package_data().as_entries()?.into_iter() { - let package = package.clone(); - if let Some(current_dependencies) = package - .as_installed() - .and_then(|x| x.as_current_dependencies().de().ok()) - { - let manifest = package.as_manifest().de()?; - all_dependency_config_errs.insert( - package_id.clone(), - compute_dependency_config_errs( - self, - &peek, - &manifest, - ¤t_dependencies, - &Default::default(), - ) - .await?, - ); + self.services.init(&self, init_services).await?; + tracing::info!("Initialized Services"); + + // TODO + check_requested_actions.start(); + let peek = self.db.peek().await; + let mut action_input: OrdMap> = OrdMap::new(); + let requested_actions: BTreeSet<_> = peek + .as_public() + .as_package_data() + .as_entries()? + .into_iter() + .map(|(_, pde)| { + Ok(pde + .as_requested_actions() + .as_entries()? + .into_iter() + .map(|(_, r)| { + Ok::<_, Error>(( + r.as_request().as_package_id().de()?, + r.as_request().as_action_id().de()?, + )) + })) + }) + .flatten_ok() + .map(|a| a.and_then(|a| a)) + .try_collect()?; + let procedure_id = Guid::new(); + for (package_id, action_id) in requested_actions { + if let Some(service) = self.services.get(&package_id).await.as_ref() { + if let Some(input) = service + .get_action_input(procedure_id.clone(), action_id.clone()) + .await? + .and_then(|i| i.value) + { + action_input + .entry(package_id) + .or_default() + .insert(action_id, input); + } } } self.db - .mutate(|v| { - for (package_id, errs) in all_dependency_config_errs { - if let Some(config_errors) = v - .as_package_data_mut() - .as_idx_mut(&package_id) - .and_then(|pde| pde.as_installed_mut()) - .map(|i| i.as_status_mut().as_dependency_config_errors_mut()) - { - config_errors.ser(&errs)?; + .mutate(|db| { + for (package_id, action_input) in &action_input { + for (action_id, input) in action_input { + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + pde.as_requested_actions_mut().mutate(|requested_actions| { + Ok(update_requested_actions( + requested_actions, + package_id, + action_id, + input, + false, + )) + })?; + } } } Ok(()) }) .await?; + check_requested_actions.complete(); Ok(()) } - - #[instrument(skip_all)] - pub async fn clean_continuations(&self) { - let mut continuations = self.rpc_stream_continuations.lock().await; - let mut to_remove = Vec::new(); - for (guid, cont) in &*continuations { - if cont.is_timed_out() { - to_remove.push(guid.clone()); - } - } - for guid in to_remove { - continuations.remove(&guid); - } - } - - #[instrument(skip_all)] - pub async fn add_continuation(&self, guid: RequestGuid, handler: RpcContinuation) { - self.clean_continuations().await; - self.rpc_stream_continuations - .lock() + pub async fn call_remote( + &self, + method: &str, + params: Value, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, Empty {}) .await - .insert(guid, handler); } - - pub async fn get_continuation_handler(&self, guid: &RequestGuid) -> Option { - let mut continuations = self.rpc_stream_continuations.lock().await; - if let Some(cont) = continuations.remove(guid) { - cont.into_handler().await - } else { - None - } - } - - pub async fn get_ws_continuation_handler(&self, guid: &RequestGuid) -> Option { - let continuations = self.rpc_stream_continuations.lock().await; - if matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { - drop(continuations); - self.get_continuation_handler(guid).await - } else { - None - } - } - - pub async fn get_rest_continuation_handler(&self, guid: &RequestGuid) -> Option { - let continuations = self.rpc_stream_continuations.lock().await; - if matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { - drop(continuations); - self.get_continuation_handler(guid).await - } else { - None - } + pub async fn call_remote_with( + &self, + method: &str, + params: Value, + extra: T, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, extra).await } } impl AsRef for RpcContext { @@ -453,6 +456,16 @@ impl AsRef for RpcContext { &CURRENT_SECRET } } +impl AsRef for RpcContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} +impl AsRef>> for RpcContext { + fn as_ref(&self) -> &OpenAuthedContinuations> { + &self.open_authed_continuations + } +} impl Context for RpcContext {} impl Deref for RpcContext { type Target = RpcContextSeed; diff --git a/core/startos/src/context/sdk.rs b/core/startos/src/context/sdk.rs index 7ba7a6bfa..fb5d99572 100644 --- a/core/startos/src/context/sdk.rs +++ b/core/startos/src/context/sdk.rs @@ -8,13 +8,6 @@ use serde::Deserialize; use tracing::instrument; use crate::prelude::*; -use crate::util::config::{load_config_from_paths, local_config_path}; - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct SdkContextConfig { - pub developer_key_path: Option, -} #[derive(Debug)] pub struct SdkContextSeed { @@ -26,7 +19,7 @@ pub struct SdkContext(Arc); impl SdkContext { /// BLOCKING #[instrument(skip_all)] - pub fn init(matches: &ArgMatches) -> Result { + pub fn init(config: ) -> Result { let local_config_path = local_config_path(); let base: SdkContextConfig = load_config_from_paths( matches @@ -48,24 +41,7 @@ impl SdkContext { }), }))) } - /// BLOCKING - #[instrument(skip_all)] - pub fn developer_key(&self) -> Result { - if !self.developer_key_path.exists() { - return Err(Error::new(eyre!("Developer Key does not exist! Please run `start-sdk init` before running this command."), crate::ErrorKind::Uninitialized)); - } - let pair = ::from_pkcs8_pem( - &std::fs::read_to_string(&self.developer_key_path)?, - ) - .with_kind(crate::ErrorKind::Pem)?; - let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| { - Error::new( - eyre!("pkcs8 key is of incorrect length"), - ErrorKind::OpenSsl, - ) - })?; - Ok(secret.into()) - } + } impl std::ops::Deref for SdkContext { type Target = SdkContextSeed; diff --git a/core/startos/src/context/setup.rs b/core/startos/src/context/setup.rs index 7ae161b01..b8e4ee968 100644 --- a/core/startos/src/context/setup.rs +++ b/core/startos/src/context/setup.rs @@ -1,26 +1,32 @@ use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; +use std::time::Duration; +use futures::{Future, StreamExt}; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; use josekit::jwk::Jwk; -use patch_db::json_ptr::JsonPointer; use patch_db::PatchDb; -use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::Context; use serde::{Deserialize, Serialize}; -use sqlx::postgres::PgConnectOptions; -use sqlx::PgPool; use tokio::sync::broadcast::Sender; -use tokio::sync::RwLock; +use tokio::sync::OnceCell; use tracing::instrument; +use ts_rs::TS; use crate::account::AccountInfo; -use crate::db::model::Database; +use crate::context::config::ServerConfig; +use crate::context::RpcContext; use crate::disk::OsPartitionInfo; -use crate::init::init_postgres; -use crate::setup::SetupStatus; -use crate::util::config::load_config_from_paths; -use crate::{Error, ResultExt}; +use crate::hostname::Hostname; +use crate::net::web_server::{UpgradableListener, WebServer, WebServerAcceptorSetter}; +use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; +use crate::setup::SetupProgress; +use crate::util::net::WebSocketExt; +use crate::MAIN_DATA; lazy_static::lazy_static! { pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| { @@ -30,113 +36,185 @@ lazy_static::lazy_static! { }); } -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] pub struct SetupResult { - pub tor_address: String, - pub lan_address: String, + pub tor_addresses: Vec, + #[ts(type = "string")] + pub hostname: Hostname, + #[ts(type = "string")] + pub lan_address: InternedString, pub root_ca: String, } - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct SetupContextConfig { - pub os_partitions: OsPartitionInfo, - pub migration_batch_rows: Option, - pub migration_prefetch_rows: Option, - pub datadir: Option, - #[serde(default)] - pub disable_encryption: bool, -} -impl SetupContextConfig { - #[instrument(skip_all)] - pub async fn load + Send + 'static>(path: Option

) -> Result { - tokio::task::spawn_blocking(move || { - load_config_from_paths( - path.as_ref() - .into_iter() - .map(|p| p.as_ref()) - .chain(std::iter::once(Path::new( - crate::util::config::DEVICE_CONFIG_PATH, - ))) - .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), - ) +impl TryFrom<&AccountInfo> for SetupResult { + type Error = Error; + fn try_from(value: &AccountInfo) -> Result { + Ok(Self { + tor_addresses: value + .tor_keys + .iter() + .map(|tor_key| format!("https://{}", tor_key.public().get_onion_address())) + .collect(), + hostname: value.hostname.clone(), + lan_address: value.hostname.lan_address(), + root_ca: String::from_utf8(value.root_ca_cert.to_pem()?)?, }) - .await - .unwrap() - } - pub fn datadir(&self) -> &Path { - self.datadir - .as_deref() - .unwrap_or_else(|| Path::new("/embassy-data")) } } pub struct SetupContextSeed { + pub webserver: WebServerAcceptorSetter, + pub config: ServerConfig, pub os_partitions: OsPartitionInfo, - pub config_path: Option, - pub migration_batch_rows: usize, - pub migration_prefetch_rows: usize, pub disable_encryption: bool, + pub progress: FullProgressTracker, + pub task: OnceCell>, + pub result: OnceCell>, pub shutdown: Sender<()>, - pub datadir: PathBuf, - pub selected_v2_drive: RwLock>, - pub cached_product_key: RwLock>>, - pub setup_status: RwLock>>, - pub setup_result: RwLock, SetupResult)>>, -} - -impl AsRef for SetupContextSeed { - fn as_ref(&self) -> &Jwk { - &*CURRENT_SECRET - } + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] pub struct SetupContext(Arc); impl SetupContext { #[instrument(skip_all)] - pub async fn init + Send + 'static>(path: Option

+
-

{{ note.key | displayEmver }}

+

{{ note.key }}

{ + return Object.entries(this.pkg.otherVersions) + .filter( + ([v, _]) => + this.exver.getFlavor(v) === this.pkg.flavor && + this.exver.compareExver(this.pkg.version, v) === 1, + ) + .reduce( + (obj, [version, info]) => ({ + ...obj, + [version]: info.releaseNotes, + }), + { + [`${this.pkg.version} (current)`]: this.pkg.releaseNotes, + }, + ) + }), + ) + + constructor( + private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, + private readonly modalCtrl: ModalController, + ) {} + + async dismiss() { + return this.modalCtrl.dismiss() + } + + isSelected(key: string): boolean { + return this.selected === key + } + + setSelected(selected: string) { + this.selected = this.isSelected(selected) ? null : selected + } + + getDocSize(key: string, { nativeElement }: ElementRef) { + return this.isSelected(key) ? nativeElement.scrollHeight : 0 + } + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts similarity index 86% rename from web/projects/marketplace/src/pages/release-notes/release-notes.module.ts rename to web/projects/marketplace/src/modals/release-notes/release-notes.module.ts index 583631dc4..41055a314 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts @@ -2,12 +2,11 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TextSpinnerComponentModule, } from '@start9labs/shared' import { TuiElementModule } from '@taiga-ui/cdk' - import { ReleaseNotesComponent } from './release-notes.component' @NgModule({ @@ -15,11 +14,11 @@ import { ReleaseNotesComponent } from './release-notes.component' CommonModule, IonicModule, TextSpinnerComponentModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TuiElementModule, ], declarations: [ReleaseNotesComponent], exports: [ReleaseNotesComponent], }) -export class ReleaseNotesModule {} +export class ReleaseNotesComponentModule {} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.html b/web/projects/marketplace/src/pages/list/categories/categories.component.html index 4e99a21c2..29613813b 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.html +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.html @@ -1,9 +1,9 @@ - {{ cat }} + {{ cat.value.name }} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.ts b/web/projects/marketplace/src/pages/list/categories/categories.component.ts index b34761079..349a3bd18 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.ts +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.ts @@ -5,6 +5,7 @@ import { Input, Output, } from '@angular/core' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'marketplace-categories', @@ -17,7 +18,7 @@ import { }) export class CategoriesComponent { @Input() - categories: readonly string[] = [] + categories!: Map @Input() category = '' @@ -29,4 +30,8 @@ export class CategoriesComponent { this.category = category this.categoryChange.emit(category) } + + originalOrder() { + return 0 + } } diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html index 0220a0a0c..0a22b8699 100644 --- a/web/projects/marketplace/src/pages/list/item/item.component.html +++ b/web/projects/marketplace/src/pages/list/item/item.component.html @@ -1,12 +1,16 @@ - + - +

- {{ pkg.manifest.title }} + {{ pkg.title }}

-

{{ pkg.manifest.description.short }}

+

{{ pkg.description.short }}

diff --git a/web/projects/marketplace/src/pages/list/item/item.module.ts b/web/projects/marketplace/src/pages/list/item/item.module.ts index dd157174e..3f943ac2d 100644 --- a/web/projects/marketplace/src/pages/list/item/item.module.ts +++ b/web/projects/marketplace/src/pages/list/item/item.module.ts @@ -4,17 +4,10 @@ import { IonicModule } from '@ionic/angular' import { RouterModule } from '@angular/router' import { SharedPipesModule } from '@start9labs/shared' import { ItemComponent } from './item.component' -import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe' @NgModule({ declarations: [ItemComponent], exports: [ItemComponent], - imports: [ - CommonModule, - IonicModule, - RouterModule, - SharedPipesModule, - MimeTypePipeModule, - ], + imports: [CommonModule, IonicModule, RouterModule, SharedPipesModule], }) export class ItemModule {} diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts b/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts deleted file mode 100644 index 49da475d9..000000000 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '../../services/marketplace.service' - -@Component({ - selector: 'release-notes', - templateUrl: './release-notes.component.html', - styleUrls: ['./release-notes.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ReleaseNotesComponent { - private readonly pkgId = getPkgId(this.route) - - private selected: string | null = null - - readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId) - - constructor( - private readonly route: ActivatedRoute, - private readonly marketplaceService: AbstractMarketplaceService, - ) {} - - isSelected(key: string): boolean { - return this.selected === key - } - - setSelected(selected: string) { - this.selected = this.isSelected(selected) ? null : selected - } - - getDocSize(key: string, { nativeElement }: ElementRef) { - return this.isSelected(key) ? nativeElement.scrollHeight : 0 - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/marketplace/src/pages/show/about/about.component.html b/web/projects/marketplace/src/pages/show/about/about.component.html index c1d76dd2c..56d363071 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.html +++ b/web/projects/marketplace/src/pages/show/about/about.component.html @@ -1,13 +1,11 @@ - - New in {{ pkg.manifest.version | displayEmver }} - +New in {{ pkg.version }} -
+
- + Past Release Notes @@ -15,13 +13,10 @@ Description -

{{ pkg.manifest.description.long }}

+

{{ pkg.description.long }}

-
+
View website diff --git a/web/projects/marketplace/src/pages/show/about/about.component.ts b/web/projects/marketplace/src/pages/show/about/about.component.ts index 6626d4fbe..e9d69ebb3 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.ts +++ b/web/projects/marketplace/src/pages/show/about/about.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { MarketplacePkg } from '../../../types' +import { ModalController } from '@ionic/angular' +import { ReleaseNotesComponent } from '../../../modals/release-notes/release-notes.component' @Component({ selector: 'marketplace-about', @@ -10,4 +12,15 @@ import { MarketplacePkg } from '../../../types' export class AboutComponent { @Input() pkg!: MarketplacePkg + + constructor(private readonly modalCtrl: ModalController) {} + + async presentModalNotes() { + const modal = await this.modalCtrl.create({ + componentProps: { pkg: this.pkg }, + component: ReleaseNotesComponent, + }) + + await modal.present() + } } diff --git a/web/projects/marketplace/src/pages/show/about/about.module.ts b/web/projects/marketplace/src/pages/show/about/about.module.ts index b48bbcbaa..a110cd300 100644 --- a/web/projects/marketplace/src/pages/show/about/about.module.ts +++ b/web/projects/marketplace/src/pages/show/about/about.module.ts @@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared' - +import { ExverPipesModule, MarkdownPipeModule } from '@start9labs/shared' import { AboutComponent } from './about.component' +import { ReleaseNotesComponentModule } from '../../../modals/release-notes/release-notes.module' @NgModule({ imports: [ @@ -12,7 +12,8 @@ import { AboutComponent } from './about.component' RouterModule, IonicModule, MarkdownPipeModule, - EmverPipesModule, + ExverPipesModule, + ReleaseNotesComponentModule, ], declarations: [AboutComponent], exports: [AboutComponent], diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.html b/web/projects/marketplace/src/pages/show/additional/additional.component.html index 8937e8c74..03a52cc94 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.html @@ -1,21 +1,10 @@ - -
- Intended to replace -
    -
  • - {{ app }} -
  • -
-
-
- Additional Info - + Other Versions

License

-

{{ manifest.license }}

+

{{ pkg.license }}

@@ -64,39 +53,39 @@

Instructions

Source Repository

-

{{ manifest['upstream-repo'] }}

+

{{ pkg.upstreamRepo }}

Wrapper Repository

-

{{ manifest['wrapper-repo'] }}

+

{{ pkg.wrapperRepo }}

Support Site

-

{{ manifest['support-site'] || 'Not provided' }}

+

{{ pkg.supportSite || 'Not provided' }}

diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.ts b/web/projects/marketplace/src/pages/show/additional/additional.component.ts index 778ea6c54..92ad42bff 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -10,15 +10,9 @@ import { ModalController, ToastController, } from '@ionic/angular' -import { - copyToClipboard, - displayEmver, - Emver, - MarkdownComponent, -} from '@start9labs/shared' +import { copyToClipboard, Exver, MarkdownComponent } from '@start9labs/shared' import { MarketplacePkg } from '../../../types' import { AbstractMarketplaceService } from '../../../services/marketplace.service' -import { ActivatedRoute } from '@angular/router' @Component({ selector: 'marketplace-additional', @@ -32,15 +26,12 @@ export class AdditionalComponent { @Output() version = new EventEmitter() - readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, - private readonly emver: Emver, + private readonly exver: Exver, private readonly marketplaceService: AbstractMarketplaceService, private readonly toastCtrl: ToastController, - private readonly route: ActivatedRoute, ) {} async copy(address: string): Promise { @@ -58,41 +49,53 @@ export class AdditionalComponent { } async presentAlertVersions() { - const alert = await this.alertCtrl.create({ - header: 'Versions', - inputs: this.pkg.versions - .sort((a, b) => -1 * (this.emver.compare(a, b) || 0)) - .map(v => ({ - name: v, // for CSS - type: 'radio', - label: displayEmver(v), // appearance on screen - value: v, // literal SEM version value - checked: this.pkg.manifest.version === v, - })), - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Ok', - handler: (version: string) => this.version.emit(version), - }, - ], - }) + const versions = Object.keys(this.pkg.otherVersions).filter( + v => this.exver.getFlavor(v) === this.pkg.flavor, + ) + + if (!versions.length) { + const alert = await this.alertCtrl.create({ + header: 'Versions', + message: 'No other versions', + }) + + await alert.present() + } else { + const alert = await this.alertCtrl.create({ + header: 'Versions', + inputs: versions + .sort((a, b) => -1 * (this.exver.compareExver(a, b) || 0)) + .map(v => ({ + name: v, // for CSS + type: 'radio', + label: v, // appearance on screen + value: v, // literal SEM version value + checked: this.pkg.version === v, + })), + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Ok', + handler: (version: string) => this.version.emit(version), + }, + ], + }) - await alert.present() + await alert.present() + } } - async presentModalMd(title: string) { + async presentModalMd(asset: 'license' | 'instructions') { const content = this.marketplaceService.fetchStatic$( - this.pkg.manifest.id, - title, - this.url, + this.pkg, + asset === 'license' ? 'LICENSE.md' : 'instructions.md', ) const modal = await this.modalCtrl.create({ - componentProps: { title, content }, + componentProps: { title: asset, content }, component: MarkdownComponent, }) diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html index 87b902603..b5d3fb4db 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html @@ -2,7 +2,7 @@

- {{ pkg['dependency-metadata'][dep.key].title }} - - (required) - (required by default) - (optional) - + {{ + pkg.dependencyMetadata[dep.key].title + ? pkg.dependencyMetadata[dep.key].title + : dep.key + }} + (optional) + + (Required) +

-

- {{ dep.value.version | displayEmver }} -

{{ dep.value.description }}

diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts index a6ecb103f..7b7f2efe7 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts @@ -11,7 +11,7 @@ export class DependenciesComponent { pkg!: MarketplacePkg getImg(key: string): string { - // @TODO fix when registry api is updated to include mimetype in icon url - return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon + const icon = this.pkg.dependencyMetadata[key]?.icon + return icon ? icon : 'assets/img/service-icons/fallback.png' } } diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts index abb3032e9..55b220d08 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts @@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { - EmverPipesModule, - ResponsiveColModule, - SharedPipesModule, -} from '@start9labs/shared' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' import { DependenciesComponent } from './dependencies.component' @@ -16,7 +12,6 @@ import { DependenciesComponent } from './dependencies.component' RouterModule, IonicModule, SharedPipesModule, - EmverPipesModule, ResponsiveColModule, ], declarations: [DependenciesComponent], diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.html b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html new file mode 100644 index 000000000..7a0f64aff --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html @@ -0,0 +1,21 @@ +Alternative Implementations + + + + + + + + +

+ {{ pkg.title }} +

+

{{ pkg.version }}

+
+
+
+
+
diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts new file mode 100644 index 000000000..4927f47a1 --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '../../../types' + +@Component({ + selector: 'marketplace-flavors', + templateUrl: 'flavors.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FlavorsComponent { + @Input() + pkgs!: MarketplacePkg[] +} diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts new file mode 100644 index 000000000..662a914fd --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' +import { FlavorsComponent } from './flavors.component' + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + IonicModule, + SharedPipesModule, + ResponsiveColModule, + ], + declarations: [FlavorsComponent], + exports: [FlavorsComponent], +}) +export class FlavorsModule {} diff --git a/web/projects/marketplace/src/pages/show/package/package.component.html b/web/projects/marketplace/src/pages/show/package/package.component.html index ca853030e..aa5fcf0d0 100644 --- a/web/projects/marketplace/src/pages/show/package/package.component.html +++ b/web/projects/marketplace/src/pages/show/package/package.component.html @@ -1,10 +1,10 @@
- +
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

+

{{ pkg.title }}

+

{{ pkg.version }}

- Released: {{ pkg['published-at'] | date: 'medium' }} + Released: {{ pkg.s9pk.publishedAt | date : 'medium' }}

diff --git a/web/projects/marketplace/src/pages/show/package/package.component.scss b/web/projects/marketplace/src/pages/show/package/package.component.scss index 9e75cfd41..d5f63c9e3 100644 --- a/web/projects/marketplace/src/pages/show/package/package.component.scss +++ b/web/projects/marketplace/src/pages/show/package/package.component.scss @@ -27,8 +27,8 @@ } .published { - margin: 0; - padding: 4px 0 12px 0; + margin: 0px; + padding: 8px 0 8px 0; font-style: italic; } diff --git a/web/projects/marketplace/src/pages/show/package/package.module.ts b/web/projects/marketplace/src/pages/show/package/package.module.ts index 665e146b5..8565a2352 100644 --- a/web/projects/marketplace/src/pages/show/package/package.module.ts +++ b/web/projects/marketplace/src/pages/show/package/package.module.ts @@ -2,13 +2,12 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, SharedPipesModule, TickerModule, } from '@start9labs/shared' import { PackageComponent } from './package.component' -import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe' @NgModule({ declarations: [PackageComponent], @@ -17,9 +16,8 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe' CommonModule, IonicModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, TickerModule, - MimeTypePipeModule, ], }) export class PackageModule {} diff --git a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts index 5b0bb52b1..ed546ada9 100644 --- a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -26,11 +26,11 @@ export class FilterPackagesPipe implements PipeTransform { distance: 16, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, ], @@ -42,19 +42,19 @@ export class FilterPackagesPipe implements PipeTransform { useExtendedSearch: true, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, { - name: 'manifest.description.short', + name: 'description.short', weight: 0.4, }, { - name: 'manifest.description.long', + name: 'description.long', weight: 0.1, }, ], @@ -71,8 +71,8 @@ export class FilterPackagesPipe implements PipeTransform { .filter(p => category === 'all' || p.categories.includes(category)) .sort((a, b) => { return ( - new Date(b['published-at']).valueOf() - - new Date(a['published-at']).valueOf() + new Date(b.s9pk.publishedAt).valueOf() - + new Date(a.s9pk.publishedAt).valueOf() ) }) } diff --git a/web/projects/marketplace/src/pipes/mime-type.pipe.ts b/web/projects/marketplace/src/pipes/mime-type.pipe.ts deleted file mode 100644 index a0dc14a00..000000000 --- a/web/projects/marketplace/src/pipes/mime-type.pipe.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NgModule, Pipe, PipeTransform } from '@angular/core' -import { MarketplacePkg } from '../types' - -@Pipe({ - name: 'mimeType', -}) -export class MimeTypePipe implements PipeTransform { - transform(pkg: MarketplacePkg): string { - if (pkg.manifest.assets.icon) { - switch (pkg.manifest.assets.icon.split('.').pop()) { - case 'png': - return `data:image/png;base64,${pkg.icon}` - case 'jpeg': - case 'jpg': - return `data:image/jpeg;base64,${pkg.icon}` - case 'gif': - return `data:image/gif;base64,${pkg.icon}` - case 'svg': - return `data:image/svg+xml;base64,${pkg.icon}` - default: - return `data:image/png;base64,${pkg.icon}` - } - } - return `data:image/png;base64,${pkg.icon}` - } -} - -@NgModule({ - declarations: [MimeTypePipe], - exports: [MimeTypePipe], -}) -export class MimeTypePipeModule {} diff --git a/web/projects/marketplace/src/public-api.ts b/web/projects/marketplace/src/public-api.ts index fef451a3e..600ceabfe 100644 --- a/web/projects/marketplace/src/public-api.ts +++ b/web/projects/marketplace/src/public-api.ts @@ -10,8 +10,6 @@ export * from './pages/list/search/search.component' export * from './pages/list/search/search.module' export * from './pages/list/skeleton/skeleton.component' export * from './pages/list/skeleton/skeleton.module' -export * from './pages/release-notes/release-notes.component' -export * from './pages/release-notes/release-notes.module' export * from './pages/show/about/about.component' export * from './pages/show/about/about.module' export * from './pages/show/additional/additional.component' @@ -20,9 +18,10 @@ export * from './pages/show/dependencies/dependencies.component' export * from './pages/show/dependencies/dependencies.module' export * from './pages/show/package/package.component' export * from './pages/show/package/package.module' +export * from './pages/show/flavors/flavors.component' +export * from './pages/show/flavors/flavors.module' export * from './pipes/filter-packages.pipe' -export * from './pipes/mime-type.pipe' export * from './services/marketplace.service' diff --git a/web/projects/marketplace/src/services/marketplace.service.ts b/web/projects/marketplace/src/services/marketplace.service.ts index af1b473d5..3fdabc426 100644 --- a/web/projects/marketplace/src/services/marketplace.service.ts +++ b/web/projects/marketplace/src/services/marketplace.service.ts @@ -12,18 +12,13 @@ export abstract class AbstractMarketplaceService { abstract getPackage$( id: string, - version: string, + version: string | null, + flavor: string | null, url?: string, - ): Observable // could be {} so need to check in show page - - abstract fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> + ): Observable abstract fetchStatic$( - id: string, - type: string, - url?: string, + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', ): Observable } diff --git a/web/projects/marketplace/src/types.ts b/web/projects/marketplace/src/types.ts index 8187d3bc0..9853cddbb 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -1,87 +1,39 @@ -import { Url } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' -export type StoreURL = string -export type StoreName = string - -export interface StoreIdentity { - url: StoreURL - name?: StoreName +export type GetPackageReq = { + id: string + version: string | null + otherVersions: 'short' } -export type Marketplace = Record - -export interface StoreData { - info: StoreInfo - packages: MarketplacePkg[] +export type GetPackageRes = T.GetPackageResponse & { + otherVersions: { [version: string]: T.PackageInfoShort } } -export interface StoreInfo { - name: StoreName - categories: string[] +export type GetPackagesReq = { + id: null + version: null + otherVersions: 'short' } -export interface MarketplacePkg { - icon: Url - license: Url - instructions: Url - manifest: MarketplaceManifest - categories: string[] - versions: string[] - 'dependency-metadata': { - [id: string]: DependencyMetadata - } - 'published-at': string +export type GetPackagesRes = { + [id: T.PackageId]: GetPackageRes } -export interface DependencyMetadata { - title: string - icon: Url - hidden: boolean +export type StoreIdentity = { + url: string + name?: string } -export interface MarketplaceManifest { - id: string - title: string - version: string - 'git-hash'?: string - description: { - short: string - long: string - } - assets: { - icon: string // ie. icon.png - } - replaces?: string[] - 'release-notes': string - license: string // type of license - 'wrapper-repo': Url - 'upstream-repo': Url - 'support-site': Url - 'marketing-site': Url - 'donation-url': Url | null - alerts: { - install: string | null - uninstall: string | null - restore: string | null - start: string | null - stop: string | null - } - dependencies: Record> -} +export type Marketplace = Record -export interface Dependency { - version: string - requirement: - | { - type: 'opt-in' - how: string - } - | { - type: 'opt-out' - how: string - } - | { - type: 'required' - } - description: string | null - config: T +export type StoreData = { + info: T.RegistryInfo + packages: MarketplacePkg[] } + +export type MarketplacePkg = T.PackageVersionInfo & + Omit & { + id: T.PackageId + version: string + flavor: string | null + } diff --git a/web/projects/setup-wizard/src/app/app-routing.module.ts b/web/projects/setup-wizard/src/app/app-routing.module.ts index aa56c382d..78088236a 100644 --- a/web/projects/setup-wizard/src/app/app-routing.module.ts +++ b/web/projects/setup-wizard/src/app/app-routing.module.ts @@ -47,7 +47,6 @@ const routes: Routes = [ RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled', preloadingStrategy: PreloadAllModules, - useHash: true, initialNavigation: 'disabled', }), ], diff --git a/web/projects/setup-wizard/src/app/app.component.ts b/web/projects/setup-wizard/src/app/app.component.ts index e4bf41f5c..e07df5870 100644 --- a/web/projects/setup-wizard/src/app/app.component.ts +++ b/web/projects/setup-wizard/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { ApiService } from './services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' @Component({ selector: 'app-root', @@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared' export class AppComponent { constructor( private readonly apiService: ApiService, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly navCtrl: NavController, ) {} @@ -21,12 +21,12 @@ export class AppComponent { let route = '/home' if (inProgress) { - route = inProgress.complete ? '/success' : '/loading' + route = inProgress.status === 'complete' ? '/success' : '/loading' } await this.navCtrl.navigateForward(route) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } } } diff --git a/web/projects/setup-wizard/src/app/app.module.ts b/web/projects/setup-wizard/src/app/app.module.ts index f2ba59012..973ce35ee 100644 --- a/web/projects/setup-wizard/src/app/app.module.ts +++ b/web/projects/setup-wizard/src/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { RouteReuseStrategy } from '@angular/router' import { HttpClientModule } from '@angular/common/http' -import { TuiRootModule } from '@taiga-ui/core' +import { TuiAlertModule, TuiRootModule } from '@taiga-ui/core' import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' import { LiveApiService } from './services/api/live-api.service' @@ -41,6 +41,7 @@ const { RecoverPageModule, TransferPageModule, TuiRootModule, + TuiAlertModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts index b5c07d37c..78c09de0e 100644 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts +++ b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts @@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { CifsModal } from './cifs-modal.page' +import { ServerBackupSelectModule } from '../server-backup-select/server-backup-select.module' @NgModule({ declarations: [CifsModal], - imports: [CommonModule, FormsModule, IonicModule], + imports: [CommonModule, FormsModule, IonicModule, ServerBackupSelectModule], exports: [CifsModal], }) export class CifsModalModule {} diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts index 7f293f5e0..f6ce0e5bc 100644 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts @@ -4,9 +4,9 @@ import { LoadingController, ModalController, } from '@ionic/angular' -import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service' +import { ApiService } from 'src/app/services/api/api.service' import { StartOSDiskInfo } from '@start9labs/shared' -import { PasswordPage } from '../password/password.page' +import { ServerBackupSelectModal } from '../server-backup-select/server-backup-select.page' @Component({ selector: 'cifs-modal', @@ -50,30 +50,29 @@ export class CifsModal { await loader.dismiss() - this.presentModalPassword(diskInfo) + this.presentModalSelectServer(diskInfo) } catch (e) { await loader.dismiss() this.presentAlertFailed() } } - private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise { - const target: CifsBackupTarget = { - ...this.cifs, - mountable: true, - 'embassy-os': diskInfo, - } - + private async presentModalSelectServer( + servers: Record, + ): Promise { const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { target }, + component: ServerBackupSelectModal, + componentProps: { + servers: Object.keys(servers).map(id => ({ id, ...servers[id] })), + }, }) modal.onDidDismiss().then(res => { if (res.role === 'success') { this.modalController.dismiss( { cifs: this.cifs, - recoveryPassword: res.data.password, + serverId: res.data.serverId, + recoveryPassword: res.data.recoveryPassword, }, 'success', ) diff --git a/web/projects/setup-wizard/src/app/modals/password/password.page.ts b/web/projects/setup-wizard/src/app/modals/password/password.page.ts index 98de93e1a..f99607002 100644 --- a/web/projects/setup-wizard/src/app/modals/password/password.page.ts +++ b/web/projects/setup-wizard/src/app/modals/password/password.page.ts @@ -1,9 +1,5 @@ import { Component, Input, ViewChild } from '@angular/core' import { IonInput, ModalController } from '@ionic/angular' -import { - CifsBackupTarget, - DiskBackupTarget, -} from 'src/app/services/api/api.service' import * as argon2 from '@start9labs/argon2' @Component({ @@ -13,7 +9,7 @@ import * as argon2 from '@start9labs/argon2' }) export class PasswordPage { @ViewChild('focusInput') elem?: IonInput - @Input() target?: CifsBackupTarget | DiskBackupTarget + @Input() passwordHash = '' @Input() storageDrive = false pwError = '' @@ -31,13 +27,8 @@ export class PasswordPage { } async verifyPw() { - if (!this.target || !this.target['embassy-os']) - this.pwError = 'No recovery target' // unreachable - try { - const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' - - argon2.verify(passwordHash, this.password) + argon2.verify(this.passwordHash, this.password) this.modalController.dismiss({ password: this.password }, 'success') } catch (e) { this.pwError = 'Incorrect password provided' @@ -55,7 +46,7 @@ export class PasswordPage { } validate() { - if (!!this.target) return (this.pwError = '') + if (!!this.passwordHash) return (this.pwError = '') if (this.passwordVer) { this.checkVer() diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts new file mode 100644 index 000000000..8305cd2a6 --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { FormsModule } from '@angular/forms' +import { ServerBackupSelectModal } from './server-backup-select.page' +import { PasswordPageModule } from '../password/password.module' + +@NgModule({ + declarations: [ServerBackupSelectModal], + imports: [CommonModule, FormsModule, IonicModule, PasswordPageModule], + exports: [ServerBackupSelectModal], +}) +export class ServerBackupSelectModule {} diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html new file mode 100644 index 000000000..37736d78e --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html @@ -0,0 +1,24 @@ + + + Select Server to Restore + + + + + + +

+ Local Hostname + : {{ server.hostname }}.local +

+

+ StartOS Version + : {{ server.version }} +

+

+ Created + : {{ server.timestamp | date : 'medium' }} +

+
+
+
diff --git a/core/startos/src/procedure/build.rs b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.scss similarity index 100% rename from core/startos/src/procedure/build.rs rename to web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.scss diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts new file mode 100644 index 000000000..9b0d8cf8b --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts @@ -0,0 +1,44 @@ +import { Component, Input } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { StartOSDiskInfoWithId } from 'src/app/services/api/api.service' +import { PasswordPage } from '../password/password.page' + +@Component({ + selector: 'server-backup-select', + templateUrl: 'server-backup-select.page.html', + styleUrls: ['server-backup-select.page.scss'], +}) +export class ServerBackupSelectModal { + @Input() servers: StartOSDiskInfoWithId[] = [] + + constructor(private readonly modalController: ModalController) {} + + cancel() { + this.modalController.dismiss() + } + + async select(server: StartOSDiskInfoWithId): Promise { + this.presentModalPassword(server) + } + + private async presentModalPassword( + server: StartOSDiskInfoWithId, + ): Promise { + const modal = await this.modalController.create({ + component: PasswordPage, + componentProps: { passwordHash: server.passwordHash }, + }) + modal.onDidDismiss().then(res => { + if (res.role === 'success') { + this.modalController.dismiss( + { + serverId: server.id, + recoveryPassword: res.data.password, + }, + 'success', + ) + } + }) + await modal.present() + } +} diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts b/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts index b4d6eb9f9..3b8323798 100644 --- a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts +++ b/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts @@ -4,10 +4,10 @@ import { ModalController, NavController, } from '@ionic/angular' +import { DiskInfo, ErrorService } from '@start9labs/shared' +import { PasswordPage } from 'src/app/modals/password/password.page' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' -import { PasswordPage } from 'src/app/modals/password/password.page' @Component({ selector: 'app-attach', @@ -21,7 +21,7 @@ export class AttachPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, private readonly modalCtrl: ModalController, private readonly loadingCtrl: LoadingController, @@ -41,7 +41,7 @@ export class AttachPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -70,7 +70,7 @@ export class AttachPage { await this.stateService.importDrive(guid, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { loader.dismiss() } diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 1a5dd042d..2961daf63 100644 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -5,15 +5,11 @@ import { ModalController, NavController, } from '@ionic/angular' -import { - ApiService, - BackupRecoverySource, - DiskRecoverySource, - DiskMigrateSource, -} from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' +import { DiskInfo, ErrorService, GuidPipe } from '@start9labs/shared' +import { ApiService } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-embassy', @@ -32,7 +28,7 @@ export class EmbassyPage { private readonly alertCtrl: AlertController, private readonly stateService: StateService, private readonly loadingCtrl: LoadingController, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly guidPipe: GuidPipe, ) {} @@ -55,21 +51,24 @@ export class EmbassyPage { const disks = await this.apiService.getDrives() if (this.stateService.setupType === 'fresh') { this.storageDrives = disks - } else if (this.stateService.setupType === 'restore') { - this.storageDrives = disks.filter( - d => - !d.partitions - .map(p => p.logicalname) - .includes( - ( - (this.stateService.recoverySource as BackupRecoverySource) - ?.target as DiskRecoverySource - )?.logicalname, - ), - ) - } else if (this.stateService.setupType === 'transfer') { - const guid = (this.stateService.recoverySource as DiskMigrateSource) - .guid + } else if ( + this.stateService.setupType === 'restore' && + this.stateService.recoverySource?.type === 'backup' + ) { + if (this.stateService.recoverySource.target.type === 'disk') { + const logicalname = + this.stateService.recoverySource.target.logicalname + this.storageDrives = disks.filter( + d => !d.partitions.map(p => p.logicalname).includes(logicalname), + ) + } else { + this.storageDrives = disks + } + } else if ( + this.stateService.setupType === 'transfer' && + this.stateService.recoverySource?.type === 'migrate' + ) { + const guid = this.stateService.recoverySource.guid this.storageDrives = disks.filter(d => { return ( d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid) @@ -77,7 +76,7 @@ export class EmbassyPage { }) } } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -101,10 +100,10 @@ export class EmbassyPage { text: 'Continue', handler: () => { // for backup recoveries - if (this.stateService.recoveryPassword) { + if (this.stateService.recoverySource?.type === 'backup') { this.setupEmbassy( drive.logicalname, - this.stateService.recoveryPassword, + this.stateService.recoverySource.password, ) } else { // for migrations and fresh setups @@ -117,8 +116,11 @@ export class EmbassyPage { await alert.present() } else { // for backup recoveries - if (this.stateService.recoveryPassword) { - this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword) + if (this.stateService.recoverySource?.type === 'backup') { + this.setupEmbassy( + drive.logicalname, + this.stateService.recoverySource.password, + ) } else { // for migrations and fresh setups this.presentModalPassword(drive.logicalname) @@ -154,9 +156,13 @@ export class EmbassyPage { await this.stateService.setupEmbassy(logicalname, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } finally { loader.dismiss() } } } + +function isDiskRecovery(source: T.RecoverySource): source is any { + return source.type === 'backup' && source.target.type === 'disk' +} diff --git a/web/projects/setup-wizard/src/app/pages/home/home.page.ts b/web/projects/setup-wizard/src/app/pages/home/home.page.ts index c0e93d18a..f3d165940 100644 --- a/web/projects/setup-wizard/src/app/pages/home/home.page.ts +++ b/web/projects/setup-wizard/src/app/pages/home/home.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' import { IonicSlides } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' -import SwiperCore, { Swiper } from 'swiper' -import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' +import SwiperCore, { Swiper } from 'swiper' SwiperCore.use([IonicSlides]) @@ -19,7 +19,7 @@ export class HomePage { constructor( private readonly api: ApiService, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -33,7 +33,7 @@ export class HomePage { await this.api.getPubKey() } catch (e: any) { this.error = true - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts index e937a7e19..498620697 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts @@ -1,12 +1,17 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { LoadingPage, ToMessagePipe } from './loading.page' +import { TuiProgressModule } from '@taiga-ui/kit' +import { LoadingPage } from './loading.page' import { LoadingPageRoutingModule } from './loading-routing.module' +import { IonicModule } from '@ionic/angular' @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule], - declarations: [LoadingPage, ToMessagePipe], + imports: [ + CommonModule, + IonicModule, + TuiProgressModule, + LoadingPageRoutingModule, + ], + declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html index fd7fcc24c..942d667a3 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -1,38 +1,22 @@ - - - - Initializing StartOS -
- - {{ progress.transferred | toMessage }} - -
-
- - - -

- - - Progress: {{ (transferred * 100).toFixed() }}% - - - {{ (progress.totalBytes / 1073741824).toFixed(2) }} GB - - -

-
-
+ +
+

+ Setting up your server +

+
+ Progress: {{ (progress.total * 100).toFixed(0) }}% +
+ +

{{ progress.message }}

+
diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss index 87bfffa33..099f7084e 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss @@ -1,3 +1,9 @@ -ion-card-title { - font-size: 42px; +section { + border-radius: 0.25rem; + padding: 3rem; + margin: 2rem; + text-align: center; + background: #e0e0e0; + color: #333; + --tui-clear-inverse: rgba(0, 0, 0, 0.1); } \ No newline at end of file diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts index 10ec47d31..b95351538 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts @@ -1,15 +1,22 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' -import { StateService } from 'src/app/services/state.service' -import { Pipe, PipeTransform } from '@angular/core' -import { BehaviorSubject } from 'rxjs' +import { ErrorService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + catchError, + EMPTY, + filter, + from, + interval, + map, + Observable, + of, + startWith, + switchMap, + take, + tap, +} from 'rxjs' import { ApiService } from 'src/app/services/api/api.service' -import { ErrorToastService, pauseFor } from '@start9labs/shared' - -type Progress = { - totalBytes: number | null - transferred: number -} @Component({ selector: 'app-loading', @@ -17,69 +24,106 @@ type Progress = { styleUrls: ['loading.page.scss'], }) export class LoadingPage { - readonly progress$ = new BehaviorSubject({ - totalBytes: null, - transferred: 0, - }) + readonly progress$ = this.getRunningStatus$().pipe( + switchMap(res => + this.api.openProgressWebsocket$(res.guid).pipe( + startWith(res.progress), + catchError((_, watch$) => { + return interval(2000).pipe( + switchMap(() => + from(this.api.getStatus()).pipe(catchError(() => EMPTY)), + ), + take(1), + switchMap(() => watch$), + ) + }), + tap(progress => { + if (progress.overall === true) { + this.getStatus() + } + }), + ), + ), + map(({ phases, overall }) => { + return { + total: getDecimal(overall), + message: phases + .filter( + ( + p, + ): p is { + name: string + progress: { + done: number + total: number | null + } + } => p.progress !== true && p.progress !== null, + ) + .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .join(','), + } + }), + ) constructor( private readonly navCtrl: NavController, private readonly api: ApiService, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, ) {} - ngOnInit() { - this.poll() - } - - async poll() { + private async getStatus(): Promise<{ + status: 'running' + guid: string + progress: T.FullProgress + } | void> { try { - const progress = await this.api.getStatus() + const res = await this.api.getStatus() - if (!progress) return - - const { - 'total-bytes': totalBytes, - 'bytes-transferred': bytesTransferred, - } = progress - - this.progress$.next({ - totalBytes, - transferred: totalBytes ? bytesTransferred / totalBytes : 0, - }) - - if (progress.complete) { + if (!res) { + this.navCtrl.navigateRoot('/home') + } else if (res.status === 'complete') { this.navCtrl.navigateForward(`/success`) - this.progress$.complete() - return + } else { + return res } - - await pauseFor(250) - - setTimeout(() => this.poll(), 0) // prevent call stack from growing } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } } -} - -@Pipe({ - name: 'toMessage', -}) -export class ToMessagePipe implements PipeTransform { - constructor(private readonly stateService: StateService) {} - transform(progress: number | null): string { - if (['fresh', 'attach'].includes(this.stateService.setupType || '')) { - return 'Setting up your server' - } + private getRunningStatus$(): Observable<{ + status: 'running' + guid: string + progress: T.FullProgress + }> { + return from(this.getStatus()).pipe( + filter(Boolean), + catchError(e => { + this.errorService.handleError(e) + return of(e) + }), + take(1), + ) + } +} - if (!progress) { - return 'Calculating size' - } else if (progress < 1) { - return 'Copying data' - } else { - return 'Finalizing' - } +function getDecimal(progress: T.Progress): number { + if (progress === true) { + return 1 + } else if (!progress || !progress.total) { + return 0 + } else { + return progress.total && progress.done / progress.total } } + +function getPhaseBytes( + progress: + | false + | { + done: number + total: number | null + }, +): string { + return progress === false ? '' : `: (${progress.done}/${progress.total})` +} diff --git a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html b/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html deleted file mode 100644 index 7f4a4e5bd..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
- -

- - StartOS backup detected -

- - -

- - No StartOS backup -

-
-
diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts index eaf04f506..7ecda048a 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts @@ -3,13 +3,13 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { UnitConversionPipesModule } from '@start9labs/shared' -import { DriveStatusComponent, RecoverPage } from './recover.page' +import { RecoverPage } from './recover.page' import { PasswordPageModule } from '../../modals/password/password.module' import { RecoverPageRoutingModule } from './recover-routing.module' import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module' @NgModule({ - declarations: [RecoverPage, DriveStatusComponent], + declarations: [RecoverPage], imports: [ CommonModule, FormsModule, diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html b/web/projects/setup-wizard/src/app/pages/recover/recover.page.html index 32f71a3ad..8b92b36d3 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.html @@ -54,29 +54,21 @@

Physical Drive

- - - + + -

{{ drive.label || drive.logicalname }}

- -

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

Capacity: {{ drive.capacity | convertBytes }}

+

+ Local Hostname + : {{ server.hostname }}.local +

+

+ StartOS Version + : {{ server.version }} +

+

+ Created + : {{ server.timestamp | date : 'medium' }} +

diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts index a8cd194ba..a3b03049e 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -1,8 +1,11 @@ -import { Component, Input } from '@angular/core' +import { Component } from '@angular/core' import { ModalController, NavController } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' -import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' +import { + ApiService, + StartOSDiskInfoFull, +} from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -13,14 +16,14 @@ import { PasswordPage } from '../../modals/password/password.page' }) export class RecoverPage { loading = true - mappedDrives: MappedDisk[] = [] + servers: StartOSDiskInfoFull[] = [] constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, private readonly modalController: ModalController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -34,35 +37,21 @@ export class RecoverPage { await this.getDrives() } - driveClickable(mapped: MappedDisk) { - return mapped.drive['embassy-os']?.full - } - async getDrives() { - this.mappedDrives = [] try { - const disks = await this.apiService.getDrives() - disks - .filter(d => d.partitions.length) - .forEach(d => { - d.partitions.forEach(p => { - const drive: DiskBackupTarget = { - vendor: d.vendor, - model: d.model, - logicalname: p.logicalname, - label: p.label, - capacity: p.capacity, - used: p.used, - 'embassy-os': p['embassy-os'], - } - this.mappedDrives.push({ - hasValidBackup: !!p['embassy-os']?.full, - drive, - }) - }) - }) + const drives = await this.apiService.getDrives() + this.servers = drives.flatMap(drive => + drive.partitions.flatMap(partition => + Object.entries(partition.startOs).map(([id, val]) => ({ + id, + ...val, + partition, + drive, + })), + ), + ) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -74,65 +63,41 @@ export class RecoverPage { }) modal.onDidDismiss().then(res => { if (res.role === 'success') { - const { hostname, path, username, password } = res.data.cifs this.stateService.recoverySource = { type: 'backup', target: { type: 'cifs', - hostname, - path, - username, - password, + ...res.data.cifs, }, + serverId: res.data.serverId, + password: res.data.recoveryPassword, } - this.stateService.recoveryPassword = res.data.recoveryPassword this.navCtrl.navigateForward('/storage') } }) await modal.present() } - async select(target: DiskBackupTarget) { - const { logicalname } = target - - if (!logicalname) return - + async select(server: StartOSDiskInfoFull) { const modal = await this.modalController.create({ component: PasswordPage, - componentProps: { target }, + componentProps: { passwordHash: server.passwordHash }, cssClass: 'alertlike-modal', }) modal.onDidDismiss().then(res => { - if (res.data?.password) { - this.selectRecoverySource(logicalname, res.data.password) + if (res.role === 'success') { + this.stateService.recoverySource = { + type: 'backup', + target: { + type: 'disk', + logicalname: server.partition.logicalname, + }, + serverId: server.id, + password: res.data.password, + } + this.navCtrl.navigateForward(`/storage`) } }) await modal.present() } - - private async selectRecoverySource(logicalname: string, password?: string) { - this.stateService.recoverySource = { - type: 'backup', - target: { - type: 'disk', - logicalname, - }, - } - this.stateService.recoveryPassword = password - this.navCtrl.navigateForward(`/storage`) - } -} - -@Component({ - selector: 'drive-status', - templateUrl: './drive-status.component.html', - styleUrls: ['./recover.page.scss'], -}) -export class DriveStatusComponent { - @Input() hasValidBackup!: boolean -} - -interface MappedDisk { - hasValidBackup: boolean - drive: DiskBackupTarget } diff --git a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html b/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html index ef8c32a49..60e25e48a 100644 --- a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html +++ b/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html @@ -121,7 +121,7 @@

overflow: auto; " > - +

diff --git a/web/projects/setup-wizard/src/app/pages/success/success.module.ts b/web/projects/setup-wizard/src/app/pages/success/success.module.ts index c0a7a0ec2..7abb5d2d8 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.module.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.module.ts @@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { ResponsiveColModule } from '@start9labs/shared' - import { SuccessPage } from './success.page' import { PasswordPageModule } from '../../modals/password/password.module' import { SuccessPageRoutingModule } from './success-routing.module' diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.ts b/web/projects/setup-wizard/src/app/pages/success/success.page.ts index c1d175aaa..198ec3387 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -1,6 +1,6 @@ import { DOCUMENT } from '@angular/common' import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core' -import { DownloadHTMLService, ErrorToastService } from '@start9labs/shared' +import { DownloadHTMLService, ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' @@ -8,14 +8,14 @@ import { StateService } from 'src/app/services/state.service' selector: 'success', templateUrl: 'success.page.html', styleUrls: ['success.page.scss'], - providers: [DownloadHTMLService], }) export class SuccessPage { @ViewChild('canvas', { static: true }) - private canvas: ElementRef = {} as ElementRef + private canvas: ElementRef = + {} as ElementRef private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D - torAddress?: string + torAddresses?: string[] lanAddress?: string cert?: string @@ -28,7 +28,7 @@ export class SuccessPage { constructor( @Inject(DOCUMENT) private readonly document: Document, - private readonly errCtrl: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, private readonly api: ApiService, private readonly downloadHtml: DownloadHTMLService, @@ -52,7 +52,7 @@ export class SuccessPage { const torAddress = this.document.getElementById('tor-addr') const lanAddress = this.document.getElementById('lan-addr') - if (torAddress) torAddress.innerHTML = this.torAddress! + if (torAddress) torAddress.innerHTML = this.torAddresses!.join('\n') if (lanAddress) lanAddress.innerHTML = this.lanAddress! this.document @@ -76,14 +76,16 @@ export class SuccessPage { try { const ret = await this.api.complete() if (!this.isKiosk) { - this.torAddress = ret['tor-address'].replace(/^https:/, 'http:') - this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:') - this.cert = ret['root-ca'] + this.torAddresses = ret.torAddresses.map(a => + a.replace(/^https:/, 'http:'), + ) + this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:') + this.cert = ret.rootCa await this.api.exit() } } catch (e: any) { - await this.errCtrl.present(e) + this.errorService.handleError(e) } } diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts index 5de21a289..5034a2272 100644 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts +++ b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { AlertController, NavController } from '@ionic/angular' +import { DiskInfo, ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' @Component({ @@ -17,7 +17,7 @@ export class TransferPage { private readonly apiService: ApiService, private readonly navCtrl: NavController, private readonly alertCtrl: AlertController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -35,7 +35,7 @@ export class TransferPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/setup-wizard/src/app/services/api/api.service.ts b/web/projects/setup-wizard/src/app/services/api/api.service.ts index df9636093..882d656ae 100644 --- a/web/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/api.service.ts @@ -1,18 +1,30 @@ import * as jose from 'node-jose' -import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared' +import { + DiskInfo, + DiskListResponse, + PartitionInfo, + StartOSDiskInfo, +} from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { Observable } from 'rxjs' + export abstract class ApiService { pubkey?: jose.JWK.Key - abstract getStatus(): Promise // setup.status + abstract getStatus(): Promise // setup.status abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list - abstract verifyCifs(cifs: CifsRecoverySource): Promise // setup.cifs.verify - abstract attach(importInfo: AttachReq): Promise // setup.attach - abstract execute(setupInfo: ExecuteReq): Promise // setup.execute - abstract complete(): Promise // setup.complete + abstract verifyCifs( + cifs: T.VerifyCifsParams, + ): Promise> // setup.cifs.verify + abstract attach(importInfo: T.AttachParams): Promise // setup.attach + abstract execute(setupInfo: T.SetupExecuteParams): Promise // setup.execute + abstract complete(): Promise // setup.complete abstract exit(): Promise // setup.exit + abstract openProgressWebsocket$(guid: string): Observable - async encrypt(toEncrypt: string): Promise { + async encrypt(toEncrypt: string): Promise { if (!this.pubkey) throw new Error('No pubkey found!') const encrypted = await jose.JWE.createEncrypt(this.pubkey!) .update(toEncrypt) @@ -23,72 +35,13 @@ export abstract class ApiService { } } -type Encrypted = { - encrypted: string -} - -export type StatusRes = { - 'bytes-transferred': number - 'total-bytes': number | null - complete: boolean -} | null - -export type AttachReq = { - guid: string - 'embassy-password': Encrypted -} - -export type ExecuteReq = { - 'embassy-logicalname': string - 'embassy-password': Encrypted - 'recovery-source': RecoverySource | null - 'recovery-password': Encrypted | null -} - -export type CompleteRes = { - 'tor-address': string - 'lan-address': string - 'root-ca': string -} - -export type DiskBackupTarget = { - vendor: string | null - model: string | null - logicalname: string | null - label: string | null - capacity: number - used: number | null - 'embassy-os': StartOSDiskInfo | null -} - -export type CifsBackupTarget = { - hostname: string - path: string - username: string - mountable: boolean - 'embassy-os': StartOSDiskInfo | null -} - -export type DiskRecoverySource = { - type: 'disk' - logicalname: string // partition logicalname -} - -export type BackupRecoverySource = { - type: 'backup' - target: CifsRecoverySource | DiskRecoverySource -} -export type RecoverySource = BackupRecoverySource | DiskMigrateSource +export type WebsocketConfig = Omit, 'url'> -export type DiskMigrateSource = { - type: 'migrate' - guid: string +export type StartOSDiskInfoWithId = StartOSDiskInfo & { + id: string } -export type CifsRecoverySource = { - type: 'cifs' - hostname: string - path: string - username: string - password: Encrypted | null +export type StartOSDiskInfoFull = StartOSDiskInfoWithId & { + partition: PartitionInfo + drive: DiskInfo } diff --git a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts index ab67db5d0..0245bf554 100644 --- a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { DiskListResponse, StartOSDiskInfo, @@ -8,27 +8,35 @@ import { RpcError, RPCOptions, } from '@start9labs/shared' -import { - ApiService, - CifsRecoverySource, - DiskRecoverySource, - StatusRes, - AttachReq, - ExecuteReq, - CompleteRes, -} from './api.service' +import { T } from '@start9labs/start-sdk' +import { ApiService } from './api.service' import * as jose from 'node-jose' +import { Observable } from 'rxjs' +import { DOCUMENT } from '@angular/common' +import { webSocket } from 'rxjs/webSocket' @Injectable({ providedIn: 'root', }) export class LiveApiService extends ApiService { - constructor(private readonly http: HttpService) { + constructor( + private readonly http: HttpService, + @Inject(DOCUMENT) private readonly document: Document, + ) { super() } - async getStatus() { - return this.rpcRequest({ + openProgressWebsocket$(guid: string): Observable { + const { location } = this.document.defaultView! + const host = location.host + + return webSocket({ + url: `ws://${host}/ws/rpc/${guid}`, + }) + } + + async getStatus(): Promise { + return this.rpcRequest({ method: 'setup.status', params: {}, }) @@ -41,7 +49,7 @@ export class LiveApiService extends ApiService { * this wil all public/private key, which means that there is no information loss * through the network. */ - async getPubKey() { + async getPubKey(): Promise { const response: jose.JWK.Key = await this.rpcRequest({ method: 'setup.get-pubkey', params: {}, @@ -50,56 +58,57 @@ export class LiveApiService extends ApiService { this.pubkey = response } - async getDrives() { + async getDrives(): Promise { return this.rpcRequest({ method: 'setup.disk.list', params: {}, }) } - async verifyCifs(source: CifsRecoverySource) { + async verifyCifs( + source: T.VerifyCifsParams, + ): Promise> { source.path = source.path.replace('/\\/g', '/') - return this.rpcRequest({ + return this.rpcRequest>({ method: 'setup.cifs.verify', params: source, }) } - async attach(params: AttachReq) { - await this.rpcRequest({ + async attach(params: T.AttachParams): Promise { + return this.rpcRequest({ method: 'setup.attach', params, }) } - async execute(setupInfo: ExecuteReq) { - if (setupInfo['recovery-source']?.type === 'backup') { - if (isCifsSource(setupInfo['recovery-source'].target)) { - setupInfo['recovery-source'].target.path = setupInfo[ - 'recovery-source' - ].target.path.replace('/\\/g', '/') + async execute(setupInfo: T.SetupExecuteParams): Promise { + if (setupInfo.recoverySource?.type === 'backup') { + if (isCifsSource(setupInfo.recoverySource.target)) { + setupInfo.recoverySource.target.path = + setupInfo.recoverySource.target.path.replace('/\\/g', '/') } } - await this.rpcRequest({ + return this.rpcRequest({ method: 'setup.execute', params: setupInfo, }) } - async complete() { - const res = await this.rpcRequest({ + async complete(): Promise { + const res = await this.rpcRequest({ method: 'setup.complete', params: {}, }) return { ...res, - 'root-ca': encodeBase64(res['root-ca']), + rootCa: encodeBase64(res.rootCa), } } - async exit() { + async exit(): Promise { await this.rpcRequest({ method: 'setup.exit', params: {}, @@ -120,7 +129,7 @@ export class LiveApiService extends ApiService { } function isCifsSource( - source: CifsRecoverySource | DiskRecoverySource | null, -): source is CifsRecoverySource { - return !!(source as CifsRecoverySource)?.hostname + source: T.BackupTargetFS | null, +): source is T.Cifs & { type: 'cifs' } { + return !!(source as T.Cifs)?.hostname } diff --git a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts index 51acf4e7b..e84ef5335 100644 --- a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -1,42 +1,152 @@ import { Injectable } from '@angular/core' -import { encodeBase64, pauseFor } from '@start9labs/shared' import { - ApiService, - AttachReq, - CifsRecoverySource, - CompleteRes, - ExecuteReq, -} from './api.service' + DiskListResponse, + StartOSDiskInfo, + encodeBase64, + pauseFor, +} from '@start9labs/shared' +import { ApiService } from './api.service' import * as jose from 'node-jose' - -let tries: number +import { T } from '@start9labs/start-sdk' +import { + Observable, + concatMap, + delay, + from, + interval, + map, + mergeScan, + of, + startWith, + switchMap, + switchScan, + takeWhile, +} from 'rxjs' @Injectable({ providedIn: 'root', }) export class MockApiService extends ApiService { - async getStatus() { - const restoreOrMigrate = true - await pauseFor(1000) + // fullProgress$(): Observable { + // const phases = [ + // { + // name: 'Preparing Data', + // progress: null, + // }, + // { + // name: 'Transferring Data', + // progress: null, + // }, + // { + // name: 'Finalizing Setup', + // progress: null, + // }, + // ] - if (tries === undefined) { - tries = 0 - return null - } + // return from(phases).pipe( + // switchScan((acc, val, i) => {}, { overall: null, phases }), + // ) + // } - tries++ + // namedProgress$(namedProgress: T.NamedProgress): Observable { + // return of(namedProgress).pipe(startWith(namedProgress)) + // } - const total = tries <= 4 ? tries * 268435456 : 1073741824 - const progress = tries > 4 ? (tries - 4) * 268435456 : 0 + // progress$(progress: T.Progress): Observable {} - return { - 'bytes-transferred': restoreOrMigrate ? progress : 0, - 'total-bytes': restoreOrMigrate ? total : null, - complete: progress === total, + // websocket + + openProgressWebsocket$(guid: string): Observable { + return of(PROGRESS) + // const numPhases = PROGRESS.phases.length + + // return of(PROGRESS).pipe( + // switchMap(full => + // from(PROGRESS.phases).pipe( + // mergeScan((full, phase, i) => { + // if ( + // !phase.progress || + // typeof phase.progress !== 'object' || + // !phase.progress.total + // ) { + // full.phases[i].progress = true + + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases + // full.overall.done += step + // } + + // return of(full).pipe(delay(2000)) + // } else { + // const total = phase.progress.total + // const step = total / 4 + // let done = phase.progress.done + + // return interval(1000).pipe( + // takeWhile(() => done < total), + // map(() => { + // done += step + + // console.error(done) + + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases / 4 + + // full.overall.done += step + // } + + // if (done === total) { + // full.phases[i].progress = true + + // if (i === numPhases - 1) { + // full.overall = true + // } + // } + // return full + // }), + // ) + // } + // }, full), + // ), + // ), + // ) + } + + private statusIndex = 0 + async getStatus(): Promise { + await pauseFor(1000) + + this.statusIndex++ + + switch (this.statusIndex) { + case 2: + return { + status: 'running', + progress: PROGRESS, + guid: 'progress-guid', + } + case 3: + return { + status: 'complete', + torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'], + hostname: 'adjective-noun', + lanAddress: 'https://adjective-noun.local', + rootCa: encodeBase64(rootCA), + } + default: + return null } } - async getPubKey() { + async getPubKey(): Promise { await pauseFor(1000) // randomly generated @@ -52,7 +162,7 @@ export class MockApiService extends ApiService { }) } - async getDrives() { + async getDrives(): Promise { await pauseFor(1000) return [ { @@ -65,12 +175,15 @@ export class MockApiService extends ApiService { label: null, capacity: 1979120929996, used: null, - 'embassy-os': { - version: '0.2.17', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -88,12 +201,15 @@ export class MockApiService extends ApiService { label: null, capacity: 73264762332, used: null, - 'embassy-os': { - version: '0.3.3', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -111,12 +227,15 @@ export class MockApiService extends ApiService { label: null, capacity: 73264762332, used: null, - 'embassy-os': { - version: '0.3.2', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: 'guid-guid-guid-guid', }, @@ -127,35 +246,51 @@ export class MockApiService extends ApiService { ] } - async verifyCifs(params: CifsRecoverySource) { + async verifyCifs( + params: T.VerifyCifsParams, + ): Promise> { await pauseFor(1000) return { - version: '0.3.0', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': '', + '9876-5432-1234-5678': { + hostname: 'adjective-noun', + version: '0.3.6', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, } } - async attach(params: AttachReq) { + async attach(params: T.AttachParams): Promise { await pauseFor(1000) + + return { + progress: PROGRESS, + guid: 'progress-guid', + } } - async execute(setupInfo: ExecuteReq) { + async execute(setupInfo: T.SetupExecuteParams): Promise { await pauseFor(1000) + + return { + progress: PROGRESS, + guid: 'progress-guid', + } } - async complete(): Promise { + async complete(): Promise { await pauseFor(1000) return { - 'tor-address': 'https://asdafsadasdasasdasdfasdfasdf.onion', - 'lan-address': 'https://adjective-noun.local', - 'root-ca': encodeBase64(rootCA), + torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'], + hostname: 'adjective-noun', + lanAddress: 'https://adjective-noun.local', + rootCa: encodeBase64(rootCA), } } - async exit() { + async exit(): Promise { await pauseFor(1000) } } @@ -182,3 +317,8 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX 2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4 -----END CERTIFICATE-----` + +const PROGRESS = { + overall: null, + phases: [], +} diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index e70478559..8f4290ac0 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -1,21 +1,20 @@ import { Injectable } from '@angular/core' -import { ApiService, RecoverySource } from './api/api.service' +import { ApiService } from './api/api.service' +import { T } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', }) export class StateService { setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' - - recoverySource?: RecoverySource - recoveryPassword?: string + recoverySource?: T.RecoverySource constructor(private readonly api: ApiService) {} async importDrive(guid: string, password: string): Promise { await this.api.attach({ guid, - 'embassy-password': await this.api.encrypt(password), + startOsPassword: await this.api.encrypt(password), }) } @@ -24,11 +23,15 @@ export class StateService { password: string, ): Promise { await this.api.execute({ - 'embassy-logicalname': storageLogicalname, - 'embassy-password': await this.api.encrypt(password), - 'recovery-source': this.recoverySource || null, - 'recovery-password': this.recoveryPassword - ? await this.api.encrypt(this.recoveryPassword) + startOsLogicalname: storageLogicalname, + startOsPassword: await this.api.encrypt(password), + recoverySource: this.recoverySource + ? this.recoverySource.type === 'migrate' + ? this.recoverySource + : { + ...this.recoverySource, + password: await this.api.encrypt(this.recoverySource.password), + } : null, }) } diff --git a/web/projects/shared/assets/icon/favicon.ico b/web/projects/shared/assets/icon/favicon.ico deleted file mode 100644 index 01b1348a1..000000000 Binary files a/web/projects/shared/assets/icon/favicon.ico and /dev/null differ diff --git a/web/projects/shared/assets/icons/apple-touch-icon.png b/web/projects/shared/assets/icons/apple-touch-icon.png new file mode 100644 index 000000000..1df19c2b4 Binary files /dev/null and b/web/projects/shared/assets/icons/apple-touch-icon.png differ diff --git a/web/projects/shared/assets/icons/favicon-96x96.png b/web/projects/shared/assets/icons/favicon-96x96.png new file mode 100644 index 000000000..4df833b3e Binary files /dev/null and b/web/projects/shared/assets/icons/favicon-96x96.png differ diff --git a/web/projects/shared/assets/icons/favicon.ico b/web/projects/shared/assets/icons/favicon.ico new file mode 100644 index 000000000..9622dd64c Binary files /dev/null and b/web/projects/shared/assets/icons/favicon.ico differ diff --git a/web/projects/shared/assets/icons/favicon.svg b/web/projects/shared/assets/icons/favicon.svg new file mode 100644 index 000000000..c310c86d3 --- /dev/null +++ b/web/projects/shared/assets/icons/favicon.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/web/projects/shared/assets/icons/web-app-manifest-192x192.png b/web/projects/shared/assets/icons/web-app-manifest-192x192.png new file mode 100644 index 000000000..24d2c68eb Binary files /dev/null and b/web/projects/shared/assets/icons/web-app-manifest-192x192.png differ diff --git a/web/projects/shared/assets/icons/web-app-manifest-512x512.png b/web/projects/shared/assets/icons/web-app-manifest-512x512.png new file mode 100644 index 000000000..8d9af255a Binary files /dev/null and b/web/projects/shared/assets/icons/web-app-manifest-512x512.png differ diff --git a/web/projects/shared/assets/img/icon_transparent.png b/web/projects/shared/assets/img/icon_transparent.png new file mode 100644 index 000000000..f0aafd15d Binary files /dev/null and b/web/projects/shared/assets/img/icon_transparent.png differ diff --git a/web/projects/shared/assets/img/service-icons/fallback.png b/web/projects/shared/assets/img/service-icons/fallback.png new file mode 100644 index 000000000..75f97cc58 Binary files /dev/null and b/web/projects/shared/assets/img/service-icons/fallback.png differ diff --git a/web/projects/shared/assets/img/storefront-outline.png b/web/projects/shared/assets/img/storefront-outline.png new file mode 100644 index 000000000..aa7bd4404 Binary files /dev/null and b/web/projects/shared/assets/img/storefront-outline.png differ diff --git a/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg b/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg new file mode 100644 index 000000000..55450c05c --- /dev/null +++ b/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg @@ -0,0 +1,10 @@ + + + diff --git a/web/projects/shared/src/components/loading/loading.component.scss b/web/projects/shared/src/components/loading/loading.component.scss new file mode 100644 index 000000000..9a7d10100 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.component.scss @@ -0,0 +1,20 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + @include shadow(3); + + display: flex; + align-items: center; + max-width: 80%; + margin: auto; + padding: 1.5rem; + background: var(--tui-elevation-01); + border-radius: var(--tui-radius-m); + + --tui-primary: var(--tui-warning-fill); +} + +tui-loader { + flex-shrink: 0; + min-width: 2rem; +} diff --git a/web/projects/shared/src/components/loading/loading.component.ts b/web/projects/shared/src/components/loading/loading.component.ts new file mode 100644 index 000000000..373f013a1 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusContent, +} from '@tinkoff/ng-polymorpheus' + +@Component({ + template: ` + + `, + styleUrls: ['./loading.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoadingComponent { + readonly content: PolymorpheusContent = + inject(POLYMORPHEUS_CONTEXT)['content'] +} diff --git a/web/projects/shared/src/components/loading/loading.module.ts b/web/projects/shared/src/components/loading/loading.module.ts new file mode 100644 index 000000000..4a3798041 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { TuiLoaderModule } from '@taiga-ui/core' +import { tuiAsDialog } from '@taiga-ui/cdk' +import { LoadingComponent } from './loading.component' +import { LoadingService } from './loading.service' + +@NgModule({ + imports: [TuiLoaderModule], + declarations: [LoadingComponent], + exports: [LoadingComponent], + providers: [tuiAsDialog(LoadingService)], +}) +export class LoadingModule {} diff --git a/web/projects/shared/src/components/loading/loading.service.ts b/web/projects/shared/src/components/loading/loading.service.ts new file mode 100644 index 000000000..96ab4301f --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core' +import { AbstractTuiDialogService } from '@taiga-ui/cdk' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { LoadingComponent } from './loading.component' + +@Injectable({ providedIn: `root` }) +export class LoadingService extends AbstractTuiDialogService { + protected readonly component = new PolymorpheusComponent(LoadingComponent) + protected readonly defaultOptions = {} +} diff --git a/web/projects/shared/src/components/markdown/markdown.component.ts b/web/projects/shared/src/components/markdown/markdown.component.ts index d560f7537..e3f350e4a 100644 --- a/web/projects/shared/src/components/markdown/markdown.component.ts +++ b/web/projects/shared/src/components/markdown/markdown.component.ts @@ -3,7 +3,7 @@ import { ModalController } from '@ionic/angular' import { defer, isObservable, Observable, of } from 'rxjs' import { catchError, ignoreElements, share } from 'rxjs/operators' -import { getErrorMessage } from '../../services/error-toast.service' +import { getErrorMessage } from '../../services/error.service' @Component({ selector: 'markdown', diff --git a/web/projects/shared/src/pipes/emver/emver.module.ts b/web/projects/shared/src/pipes/emver/emver.module.ts deleted file mode 100644 index 87b86e8cb..000000000 --- a/web/projects/shared/src/pipes/emver/emver.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { - EmverComparesPipe, - EmverDisplayPipe, - EmverSatisfiesPipe, -} from './emver.pipe' - -@NgModule({ - declarations: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe], - exports: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe], -}) -export class EmverPipesModule {} diff --git a/web/projects/shared/src/pipes/emver/emver.pipe.ts b/web/projects/shared/src/pipes/emver/emver.pipe.ts deleted file mode 100644 index 183c4a0a8..000000000 --- a/web/projects/shared/src/pipes/emver/emver.pipe.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { Emver } from '../../services/emver.service' - -@Pipe({ - name: 'satisfiesEmver', -}) -export class EmverSatisfiesPipe implements PipeTransform { - constructor(private readonly emver: Emver) {} - - transform(versionUnderTest?: string, range?: string): boolean { - return ( - !!versionUnderTest && - !!range && - this.emver.satisfies(versionUnderTest, range) - ) - } -} - -@Pipe({ - name: 'compareEmver', -}) -export class EmverComparesPipe implements PipeTransform { - constructor(private readonly emver: Emver) {} - - transform(first: string, second: string): SemverResult { - try { - return this.emver.compare(first, second) as SemverResult - } catch (e) { - console.error(`emver comparison failed`, e, first, second) - return 'comparison-impossible' - } - } -} -// left compared to right - if 1, version on left is higher; if 0, values the same; if -1, version on left is lower -type SemverResult = 0 | 1 | -1 | 'comparison-impossible' - -@Pipe({ - name: 'displayEmver', -}) -export class EmverDisplayPipe implements PipeTransform { - constructor() {} - - transform(version: string): string { - return displayEmver(version) - } -} - -export function displayEmver(version: string): string { - const vs = version.split('.') - if (vs.length === 4) return `${vs[0]}.${vs[1]}.${vs[2]}~${vs[3]}` - return version -} diff --git a/web/projects/shared/src/pipes/exver/exver.module.ts b/web/projects/shared/src/pipes/exver/exver.module.ts new file mode 100644 index 000000000..8fd90e429 --- /dev/null +++ b/web/projects/shared/src/pipes/exver/exver.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core' +import { ExverComparesPipe, ExverSatisfiesPipe } from './exver.pipe' + +@NgModule({ + declarations: [ExverComparesPipe, ExverSatisfiesPipe], + exports: [ExverComparesPipe, ExverSatisfiesPipe], +}) +export class ExverPipesModule {} diff --git a/web/projects/shared/src/pipes/exver/exver.pipe.ts b/web/projects/shared/src/pipes/exver/exver.pipe.ts new file mode 100644 index 000000000..8c998aaf9 --- /dev/null +++ b/web/projects/shared/src/pipes/exver/exver.pipe.ts @@ -0,0 +1,35 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { Exver } from '../../services/exver.service' + +@Pipe({ + name: 'satisfiesExver', +}) +export class ExverSatisfiesPipe implements PipeTransform { + constructor(private readonly exver: Exver) {} + + transform(versionUnderTest?: string, range?: string): boolean { + return ( + !!versionUnderTest && + !!range && + this.exver.satisfies(versionUnderTest, range) + ) + } +} + +@Pipe({ + name: 'compareExver', +}) +export class ExverComparesPipe implements PipeTransform { + constructor(private readonly exver: Exver) {} + + transform(first: string, second: string): SemverResult { + try { + return this.exver.compareExver(first, second) as SemverResult + } catch (e) { + console.error(`exver comparison failed`, e, first, second) + return 'comparison-impossible' + } + } +} +// left compared to right - if 1, version on left is higher; if 0, values the same; if -1, version on left is lower +type SemverResult = 0 | 1 | -1 | 'comparison-impossible' diff --git a/web/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts b/web/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts index 266e7fe9a..afdc48c48 100644 --- a/web/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts +++ b/web/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts @@ -6,13 +6,17 @@ import { Pipe, PipeTransform } from '@angular/core' }) export class ConvertBytesPipe implements PipeTransform { transform(bytes: number): string { - if (bytes === 0) return '0 Bytes' + return convertBytes(bytes) + } +} - const k = 1024 - const i = Math.floor(Math.log(bytes) / Math.log(k)) +export function convertBytes(bytes: number): string { + if (bytes === 0) return '0 Bytes' - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] - } + const k = 1024 + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } @Pipe({ diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index 7b1cd71b8..bb0401ef3 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -9,6 +9,9 @@ export * from './components/alert/alert.component' export * from './components/alert/alert.module' export * from './components/alert/alert-button.directive' export * from './components/alert/alert-input.directive' +export * from './components/loading/loading.component' +export * from './components/loading/loading.module' +export * from './components/loading/loading.service' export * from './components/markdown/markdown.component' export * from './components/markdown/markdown.component.module' export * from './components/text-spinner/text-spinner.component' @@ -27,8 +30,8 @@ export * from './directives/safe-links/safe-links.module' export * from './directives/enter/enter.directive' export * from './directives/enter/enter.module' -export * from './pipes/emver/emver.module' -export * from './pipes/emver/emver.pipe' +export * from './pipes/exver/exver.module' +export * from './pipes/exver/exver.pipe' export * from './pipes/guid/guid.module' export * from './pipes/guid/guid.pipe' export * from './pipes/markdown/markdown.module' @@ -41,8 +44,8 @@ export * from './pipes/unit-conversion/unit-conversion.module' export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './services/download-html.service' -export * from './services/emver.service' -export * from './services/error-toast.service' +export * from './services/exver.service' +export * from './services/error.service' export * from './services/http.service' export * from './themes/dark-theme/dark-theme.component' @@ -63,6 +66,7 @@ export * from './util/base-64' export * from './util/copy-to-clipboard' export * from './util/get-new-entries' export * from './util/get-pkg-id' +export * from './util/invert' export * from './util/misc.util' export * from './util/rpc.util' export * from './util/to-local-iso-string' diff --git a/web/projects/shared/src/services/download-html.service.ts b/web/projects/shared/src/services/download-html.service.ts index 81f7b945b..13a146186 100644 --- a/web/projects/shared/src/services/download-html.service.ts +++ b/web/projects/shared/src/services/download-html.service.ts @@ -1,7 +1,9 @@ import { DOCUMENT } from '@angular/common' import { Inject, Injectable } from '@angular/core' -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class DownloadHTMLService { constructor(@Inject(DOCUMENT) private readonly document: Document) {} diff --git a/web/projects/shared/src/services/emver.service.ts b/web/projects/shared/src/services/emver.service.ts deleted file mode 100644 index 8dda885ac..000000000 --- a/web/projects/shared/src/services/emver.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@angular/core' -import * as emver from '@start9labs/emver' - -@Injectable({ - providedIn: 'root', -}) -export class Emver { - constructor() {} - - compare(lhs: string, rhs: string): number | null { - if (!lhs || !rhs) return null - return emver.compare(lhs, rhs) - } - - satisfies(version: string, range: string): boolean { - return emver.satisfies(version, range) - } -} diff --git a/web/projects/shared/src/services/error-toast.service.ts b/web/projects/shared/src/services/error-toast.service.ts deleted file mode 100644 index fe6607995..000000000 --- a/web/projects/shared/src/services/error-toast.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable } from '@angular/core' -import { IonicSafeString, ToastController } from '@ionic/angular' -import { HttpError } from '../classes/http-error' - -@Injectable({ - providedIn: 'root', -}) -export class ErrorToastService { - private toast?: HTMLIonToastElement - - constructor(private readonly toastCtrl: ToastController) {} - - async present(e: HttpError | string, link?: string): Promise { - console.error(e) - - if (this.toast) return - - this.toast = await this.toastCtrl.create({ - header: 'Error', - message: getErrorMessage(e, link), - duration: 0, - position: 'top', - cssClass: 'error-toast', - buttons: [ - { - side: 'end', - icon: 'close', - handler: () => { - this.dismiss() - }, - }, - ], - }) - await this.toast.present() - } - - async dismiss(): Promise { - if (this.toast) { - await this.toast.dismiss() - this.toast = undefined - } - } -} - -export function getErrorMessage( - e: HttpError | string, - link?: string, -): string | IonicSafeString { - let message = '' - - if (typeof e === 'string') { - message = e - } else if (e.code === 0) { - message = - 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' - link = 'https://docs.start9.com/0.3.5.x/support/common-issues#request-error' - } else if (!e.message) { - message = 'Unknown Error' - } else { - message = e.message - } - - if (link) { - return new IonicSafeString( - `${message}

Get Help`, - ) - } - - return message -} diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts new file mode 100644 index 000000000..383b3fc97 --- /dev/null +++ b/web/projects/shared/src/services/error.service.ts @@ -0,0 +1,42 @@ +import { ErrorHandler, inject, Injectable } from '@angular/core' +import { TuiAlertService, TuiNotification } from '@taiga-ui/core' +import { HttpError } from '../classes/http-error' + +// TODO: Enable this as ErrorHandler +@Injectable({ + providedIn: 'root', +}) +export class ErrorService extends ErrorHandler { + private readonly alerts = inject(TuiAlertService) + + override handleError(error: HttpError | string, link?: string) { + console.error(error) + + this.alerts + .open(getErrorMessage(error, link), { + label: 'Error', + status: TuiNotification.Error, + }) + .subscribe() + } +} + +export function getErrorMessage(e: HttpError | string, link?: string): string { + let message = '' + + if (typeof e === 'string') { + message = e + } else if (e.code === 0) { + message = + 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' + } else if (!e.message) { + message = 'Unknown Error' + link = 'https://docs.start9.com/latest/support/faq' + } else { + message = e.message + } + + return link + ? `${message}

Get Help` + : message +} diff --git a/web/projects/shared/src/services/exver.service.ts b/web/projects/shared/src/services/exver.service.ts new file mode 100644 index 000000000..516879333 --- /dev/null +++ b/web/projects/shared/src/services/exver.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core' +import { ExtendedVersion, VersionRange } from '@start9labs/start-sdk' + +@Injectable({ + providedIn: 'root', +}) +export class Exver { + constructor() {} + + compareExver(lhs: string, rhs: string): number | null { + if (!lhs || !rhs) return null + try { + return ExtendedVersion.parse(lhs).compareForSort( + ExtendedVersion.parse(rhs), + ) + } catch (e) { + return null + } + } + + greaterThanOrEqual(lhs: string, rhs: string): boolean | null { + if (!lhs || !rhs) return null + try { + return ExtendedVersion.parse(lhs).greaterThanOrEqual( + ExtendedVersion.parse(rhs), + ) + } catch (e) { + return null + } + } + + satisfies(version: string, range: string): boolean { + return ExtendedVersion.parse(version).satisfies(VersionRange.parse(range)) + } + + getFlavor(version: string): string | null { + return ExtendedVersion.parse(version).flavor + } +} diff --git a/web/projects/shared/src/types/api.ts b/web/projects/shared/src/types/api.ts index 1473dc16a..c53ed5e33 100644 --- a/web/projects/shared/src/types/api.ts +++ b/web/projects/shared/src/types/api.ts @@ -6,13 +6,14 @@ export type ServerLogsReq = { export type LogsRes = { entries: Log[] - 'start-cursor'?: string - 'end-cursor'?: string + startCursor?: string + endCursor?: string } export interface Log { timestamp: string message: string + bootId: string } export type DiskListResponse = DiskInfo[] @@ -31,13 +32,14 @@ export interface PartitionInfo { label: string | null capacity: number used: number | null - 'embassy-os': StartOSDiskInfo | null + startOs: Record guid: string | null } export type StartOSDiskInfo = { + hostname: string version: string - full: boolean - 'password-hash': string | null - 'wrapped-key': string | null + timestamp: string + passwordHash: string | null + wrappedKey: string | null } diff --git a/web/projects/shared/src/types/http.types.ts b/web/projects/shared/src/types/http.types.ts index 1ae2bcc50..b704248ce 100644 --- a/web/projects/shared/src/types/http.types.ts +++ b/web/projects/shared/src/types/http.types.ts @@ -5,6 +5,8 @@ export enum Method { POST = 'POST', } +type ParamPrimitive = string | number | boolean + export interface HttpOptions { method: Method url: string @@ -12,7 +14,7 @@ export interface HttpOptions { [header: string]: string | string[] } params?: { - [param: string]: string | string[] + [param: string]: ParamPrimitive | ParamPrimitive[] } responseType?: 'json' | 'text' | 'arrayBuffer' body?: any @@ -28,7 +30,7 @@ export interface HttpAngularOptions { [header: string]: string | string[] } params?: { - [param: string]: string | string[] + [param: string]: ParamPrimitive | ParamPrimitive[] } responseType?: 'json' | 'text' | 'arrayBuffer' } diff --git a/web/projects/shared/src/types/workspace-config.ts b/web/projects/shared/src/types/workspace-config.ts index 101af40fd..0586b3560 100644 --- a/web/projects/shared/src/types/workspace-config.ts +++ b/web/projects/shared/src/types/workspace-config.ts @@ -2,7 +2,7 @@ export type WorkspaceConfig = { gitHash: string useMocks: boolean enableWidgets: boolean - // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard, diagnostic-ui + // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard ui: { api: { url: string @@ -13,7 +13,7 @@ export type WorkspaceConfig = { community: 'https://community-registry.start9.com/' } mocks: { - maskAs: 'tor' | 'local' | 'localhost' + maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet' // enables local development in secure mode maskAsHttps: boolean skipStartupAlerts: boolean diff --git a/web/projects/shared/src/util/invert.ts b/web/projects/shared/src/util/invert.ts new file mode 100644 index 000000000..6931b3660 --- /dev/null +++ b/web/projects/shared/src/util/invert.ts @@ -0,0 +1,12 @@ +export function invert< + T extends string | number | symbol, + D extends string | number | symbol, +>(obj: Record): Record { + const result = {} as Record + + for (const key in obj) { + result[obj[key]] = key + } + + return result +} diff --git a/web/projects/ui/src/app/app-routing.module.ts b/web/projects/ui/src/app/app-routing.module.ts index b1b79c05d..e16085a97 100644 --- a/web/projects/ui/src/app/app-routing.module.ts +++ b/web/projects/ui/src/app/app-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core' import { PreloadAllModules, RouterModule, Routes } from '@angular/router' +import { stateNot } from 'src/app/services/state.service' import { AuthGuard } from './guards/auth.guard' import { UnauthGuard } from './guards/unauth.guard' @@ -11,19 +12,33 @@ const routes: Routes = [ }, { path: 'login', - canActivate: [UnauthGuard], + canActivate: [UnauthGuard, stateNot(['error', 'initializing'])], loadChildren: () => import('./pages/login/login.module').then(m => m.LoginPageModule), }, + { + path: 'diagnostic', + canActivate: [stateNot(['initializing', 'running'])], + loadChildren: () => + import('./pages/diagnostic-routes/diagnostic-routing.module').then( + m => m.DiagnosticModule, + ), + }, + { + path: 'initializing', + canActivate: [stateNot(['error', 'running'])], + loadChildren: () => + import('./pages/init/init.module').then(m => m.InitPageModule), + }, { path: 'home', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule), }, { path: 'system', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/server-routes/server-routing.module').then( @@ -32,14 +47,14 @@ const routes: Routes = [ }, { path: 'updates', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/updates/updates.module').then(m => m.UpdatesPageModule), }, { path: 'marketplace', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then( @@ -48,7 +63,7 @@ const routes: Routes = [ }, { path: 'notifications', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], loadChildren: () => import('./pages/notifications/notifications.module').then( m => m.NotificationsPageModule, @@ -56,22 +71,13 @@ const routes: Routes = [ }, { path: 'services', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/apps-routes/apps-routing.module').then( m => m.AppsRoutingModule, ), }, - { - path: 'developer', - canActivate: [AuthGuard], - canActivateChild: [AuthGuard], - loadChildren: () => - import('./pages/developer-routes/developer-routing.module').then( - m => m.DeveloperRoutingModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/app.component.html b/web/projects/ui/src/app/app.component.html index 0506d5214..0d9fb860f 100644 --- a/web/projects/ui/src/app/app.component.html +++ b/web/projects/ui/src/app/app.component.html @@ -15,6 +15,7 @@ type="overlay" side="start" class="left-menu" + [class.left-menu_hidden]="withoutMenu" > diff --git a/web/projects/ui/src/app/app.component.scss b/web/projects/ui/src/app/app.component.scss index 55135b1e5..aedbdc6c4 100644 --- a/web/projects/ui/src/app/app.component.scss +++ b/web/projects/ui/src/app/app.component.scss @@ -9,11 +9,15 @@ tui-root { .left-menu { --side-max-width: 280px; + + &_hidden { + display: none; + } } .menu { :host-context(body[data-theme='Light']) & { - --ion-color-base: #F4F4F5 !important; + --ion-color-base: #f4f4f5 !important; } } diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 5675fbf5c..ddf8c074f 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, inject, OnDestroy } from '@angular/core' +import { IsActiveMatchOptions, Router } from '@angular/router' import { combineLatest, map, merge, startWith } from 'rxjs' import { AuthService } from './services/auth.service' import { SplitPaneTracker } from './services/split-pane.service' @@ -15,6 +16,13 @@ import { THEME } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { DataModel } from './services/patch-db/data-model' +const OPTIONS: IsActiveMatchOptions = { + paths: 'subset', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', +} + @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -27,15 +35,14 @@ export class AppComponent implements OnDestroy { readonly theme$ = inject(THEME) readonly offline$ = combineLatest([ this.authService.isVerified$, - this.connection.connected$, + this.connection$, this.patch - .watch$('server-info', 'status-info') - .pipe(startWith({ restarting: false, 'shutting-down': false })), + .watch$('serverInfo', 'statusInfo') + .pipe(startWith({ restarting: false, shuttingDown: false })), ]).pipe( map( ([verified, connected, status]) => - verified && - (!connected || status.restarting || status['shutting-down']), + verified && (!connected || status.restarting || status.shuttingDown), ), ) @@ -45,8 +52,9 @@ export class AppComponent implements OnDestroy { private readonly patchMonitor: PatchMonitorService, private readonly splitPane: SplitPaneTracker, private readonly patch: PatchDB, + private readonly router: Router, readonly authService: AuthService, - readonly connection: ConnectionService, + readonly connection$: ConnectionService, readonly clientStorageService: ClientStorageService, readonly themeSwitcher: ThemeSwitcherService, ) {} @@ -57,6 +65,13 @@ export class AppComponent implements OnDestroy { .subscribe(name => this.titleService.setTitle(name || 'StartOS')) } + get withoutMenu(): boolean { + return ( + this.router.isActive('initializing', OPTIONS) || + this.router.isActive('diagnostic', OPTIONS) + ) + } + splitPaneVisible({ detail }: any) { this.splitPane.sidebarOpen$.next(detail.visible) } diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index 324300851..eb199d8a7 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -1,4 +1,5 @@ import { + TuiAlertModule, TuiDialogModule, TuiModeModule, TuiRootModule, @@ -13,6 +14,7 @@ import { DarkThemeModule, EnterModule, LightThemeModule, + LoadingModule, MarkdownModule, ResponsiveColModule, SharedPipesModule, @@ -20,14 +22,11 @@ import { import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' -import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' -import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module' import { MarketplaceModule } from './marketplace.module' import { PreloaderModule } from './app/preloader/preloader.module' import { FooterModule } from './app/footer/footer.module' import { MenuModule } from './app/menu/menu.module' import { APP_PROVIDERS } from './app.providers' -import { PatchDbModule } from './services/patch-db/patch-db.module' import { ToastContainerModule } from './components/toast-container/toast-container.module' import { ConnectionBarComponentModule } from './components/connection-bar/connection-bar.component.module' import { WidgetsPageModule } from './pages/widgets/widgets.module' @@ -47,17 +46,16 @@ import { environment } from '../environments/environment' PreloaderModule, FooterModule, EnterModule, - OSWelcomePageModule, MarkdownModule, - GenericInputComponentModule, + LoadingModule, MonacoEditorModule, SharedPipesModule, MarketplaceModule, - PatchDbModule, ToastContainerModule, ConnectionBarComponentModule, TuiRootModule, TuiDialogModule, + TuiAlertModule, TuiModeModule, TuiThemeNightModule, WidgetsPageModule, diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 5d0ccc4f2..101d402b3 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -1,8 +1,16 @@ -import { APP_INITIALIZER, Provider } from '@angular/core' +import { APP_INITIALIZER, inject, Provider } from '@angular/core' import { UntypedFormBuilder } from '@angular/forms' import { Router, RouteReuseStrategy } from '@angular/router' import { IonicRouteStrategy, IonNav } from '@ionic/angular' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' +import { TUI_DIALOGS_CLOSE, TUI_ICONS_PATH } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' +import { filter, pairwise } from 'rxjs' +import { + PATCH_CACHE, + PatchDbSource, +} from 'src/app/services/patch-db/patch-db-source' +import { StateService } from 'src/app/services/state.service' import { ApiService } from './services/api/embassy-api.service' import { MockApiService } from './services/api/embassy-mock-api.service' import { LiveApiService } from './services/api/embassy-live-api.service' @@ -10,6 +18,7 @@ import { AuthService } from './services/auth.service' import { ClientStorageService } from './services/client-storage.service' import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe' import { ThemeSwitcherService } from './services/theme-switcher.service' +import { StorageService } from './services/storage.service' const { useMocks, @@ -28,9 +37,14 @@ export const APP_PROVIDERS: Provider[] = [ provide: ApiService, useClass: useMocks ? MockApiService : LiveApiService, }, + { + provide: PatchDB, + deps: [PatchDbSource, PATCH_CACHE], + useClass: PatchDB, + }, { provide: APP_INITIALIZER, - deps: [AuthService, ClientStorageService, Router], + deps: [StorageService, AuthService, ClientStorageService, Router], useFactory: appInitializer, multi: true, }, @@ -42,14 +56,31 @@ export const APP_PROVIDERS: Provider[] = [ provide: THEME, useExisting: ThemeSwitcherService, }, + { + provide: TUI_ICONS_PATH, + useValue: (name: string) => `/assets/taiga-ui/icons/${name}.svg#${name}`, + }, + { + provide: TUI_DIALOGS_CLOSE, + useFactory: () => + inject(StateService).pipe( + pairwise(), + filter( + ([prev, curr]) => + prev === 'running' && (curr === 'error' || curr === 'initializing'), + ), + ), + }, ] export function appInitializer( + storage: StorageService, auth: AuthService, localStorage: ClientStorageService, router: Router, ): () => void { return () => { + storage.migrate036() auth.init() localStorage.init() router.initialNavigation() diff --git a/web/projects/ui/src/app/app/footer/footer.component.html b/web/projects/ui/src/app/app/footer/footer.component.html index 0d5987a8e..c62a8c2de 100644 --- a/web/projects/ui/src/app/app/footer/footer.component.html +++ b/web/projects/ui/src/app/app/footer/footer.component.html @@ -5,16 +5,16 @@ > - + - Downloading: {{ getProgress(progress.size, progress.downloaded) }}% + Downloading: {{ getProgress(progress.overall.total, progress.overall.done) }}% diff --git a/web/projects/ui/src/app/app/footer/footer.component.ts b/web/projects/ui/src/app/app/footer/footer.component.ts index 9a16f5a47..adbb27fe3 100644 --- a/web/projects/ui/src/app/app/footer/footer.component.ts +++ b/web/projects/ui/src/app/app/footer/footer.component.ts @@ -13,7 +13,7 @@ import { DataModel } from '../../services/patch-db/data-model' }) export class FooterComponent { readonly progress$ = this.patch - .watch$('server-info', 'status-info', 'update-progress') + .watch$('serverInfo', 'statusInfo', 'updateProgress') .pipe(map(a => a && { ...a })) readonly animation = { diff --git a/web/projects/ui/src/app/app/menu/menu.component.html b/web/projects/ui/src/app/app/menu/menu.component.html index a54033b67..0814c81ad 100644 --- a/web/projects/ui/src/app/app/menu/menu.component.html +++ b/web/projects/ui/src/app/app/menu/menu.component.html @@ -23,7 +23,7 @@ {{ page.title }} this.patch.watch$('package-data').pipe(first())), + switchMap(() => this.patch.watch$('packageData').pipe(first())), switchMap(outer => - this.patch.watch$('package-data').pipe( + this.patch.watch$('packageData').pipe( pairwise(), filter(([prev, curr]) => Object.values(prev).some( p => - p['install-progress'] && - !curr[p.manifest.id]?.['install-progress'], + ['installing', 'updating', 'restoring'].includes( + p.stateInfo.state, + ) && + ['installed', 'removing'].includes( + curr[getManifest(p).id].stateInfo.state, + ), ), ), map(([_, curr]) => curr), @@ -95,11 +99,12 @@ export class MenuComponent { ]).pipe( map(([marketplace, local]) => Object.entries(marketplace).reduce((list, [_, store]) => { - store?.packages.forEach(({ manifest: { id, version } }) => { + store?.packages.forEach(({ id, version }) => { if ( - this.emver.compare( + local[id] && + this.exver.compareExver( version, - local[id]?.installed?.manifest.version || '', + getManifest(local[id]).version || '', ) === 1 ) list.add(id) @@ -114,19 +119,13 @@ export class MenuComponent { readonly theme$ = inject(THEME) - readonly warning$ = merge( - of(this.config.isTorHttp()), - this.patch.watch$('server-info', 'ntp-synced').pipe(map(synced => !synced)), - ) - constructor( private readonly patch: PatchDB, private readonly eosService: EOSService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, - private readonly emver: Emver, - private readonly connectionService: ConnectionService, - private readonly config: ConfigService, + private readonly exver: Exver, + private readonly connection$: ConnectionService, ) {} } diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.html b/web/projects/ui/src/app/app/preloader/preloader.component.html index 94d6d7107..b94b3fa0c 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.html +++ b/web/projects/ui/src/app/app/preloader/preloader.component.html @@ -3,13 +3,11 @@ - + - - @@ -19,14 +17,11 @@ - - - - - - - - - load bold font - - @@ -66,10 +53,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
-

a

a

a

diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.ts b/web/projects/ui/src/app/app/preloader/preloader.component.ts index 9823ac981..772c5c40b 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.component.ts @@ -1,4 +1,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' +import { FormControl } from '@angular/forms' +import { + ActionSheetController, + AlertController, + ModalController, + ToastController, +} from '@ionic/angular' // TODO: Turn into DI token if this is needed someplace else too const ICONS = [ @@ -38,10 +45,8 @@ const ICONS = [ 'eye-off-outline', 'eye-outline', 'file-tray-stacked-outline', - 'finger-print-outline', + 'finger-print', 'flash-outline', - 'flask-outline', - 'flash-off-outline', 'folder-open-outline', 'globe-outline', 'grid-outline', @@ -60,7 +65,6 @@ const ICONS = [ 'options-outline', 'pencil', 'phone-portrait-outline', - 'play-circle-outline', 'play-outline', 'power', 'pricetag-outline', @@ -70,6 +74,7 @@ const ICONS = [ 'receipt-outline', 'refresh', 'reload', + 'reload-circle-outline', 'remove', 'remove-circle-outline', 'remove-outline', @@ -89,6 +94,26 @@ const ICONS = [ 'wifi', ] +const TAIGA = [ + 'tuiIconPaintOutline', + 'tuiIconTrash', + 'tuiIconTrashOutline', + 'tuiIconChevronDown', + 'tuiIconChevronDownOutline', + 'tuiIconRefreshCcw', + 'tuiIconRefreshCcwOutline', + 'tuiIconEye', + 'tuiIconEyeOutline', + 'tuiIconEyeOff', + 'tuiIconEyeOffOutline', + 'tuiIconPlus', + 'tuiIconMinus', + 'tuiIconCheck', + 'tuiIconClose', + 'tuiIconCalendarLarge', + 'tuiIconHelpCircle', +] + @Component({ selector: 'section[appPreloader]', templateUrl: 'preloader.component.html', @@ -96,4 +121,13 @@ const ICONS = [ }) export class PreloaderComponent { readonly icons = ICONS + readonly taiga = TAIGA + readonly control = new FormControl() + + constructor( + _modals: ModalController, + _alerts: AlertController, + _toasts: ToastController, + _actions: ActionSheetController, + ) {} } diff --git a/web/projects/ui/src/app/app/preloader/preloader.module.ts b/web/projects/ui/src/app/app/preloader/preloader.module.ts index b1496e638..0f22efb25 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.module.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.module.ts @@ -1,11 +1,67 @@ import { CommonModule } from '@angular/common' import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core' +import { ReactiveFormsModule } from '@angular/forms' import { IonicModule } from '@ionic/angular' +import { + TuiErrorModule, + TuiExpandModule, + TuiLinkModule, + TuiScrollbarModule, + TuiSvgModule, + TuiTooltipModule, +} from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCellModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { + TuiElasticContainerModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputTimeModule, + TuiMultiSelectModule, + TuiProgressModule, + TuiRadioListModule, + TuiSelectModule, + TuiTextareaModule, + TuiToggleModule, +} from '@taiga-ui/kit' import { QrCodeModule } from 'ng-qrcode' import { PreloaderComponent } from './preloader.component' @NgModule({ - imports: [CommonModule, IonicModule, QrCodeModule], + imports: [ + CommonModule, + ReactiveFormsModule, + IonicModule, + QrCodeModule, + TuiTooltipModule, + TuiErrorModule, + TuiInputModule, + TuiSvgModule, + TuiIconModule, + TuiButtonModule, + TuiLinkModule, + TuiInputTimeModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiMultiSelectModule, + TuiInputNumberModule, + TuiExpandModule, + TuiSelectModule, + TuiTextareaModule, + TuiToggleModule, + TuiElasticContainerModule, + TuiCellModule, + TuiProgressModule, + TuiScrollbarModule, + TuiRadioListModule, + ], declarations: [PreloaderComponent], exports: [PreloaderComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/web/projects/ui/src/app/app/snek/snek.directive.ts b/web/projects/ui/src/app/app/snek/snek.directive.ts index 255926792..3e3ad5dc3 100644 --- a/web/projects/ui/src/app/app/snek/snek.directive.ts +++ b/web/projects/ui/src/app/app/snek/snek.directive.ts @@ -1,6 +1,6 @@ import { Directive, HostListener, Input } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ModalController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { SnakePage } from '../../modals/snake/snake.page' import { ApiService } from '../../services/api/embassy-api.service' @@ -13,8 +13,8 @@ export class SnekDirective { constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -30,22 +30,17 @@ export class SnekDirective { modal.onDidDismiss().then(async ({ data }) => { if (data?.highScore <= (this.appSnekHighScore || 0)) return - const loader = await this.loadingCtrl.create({ - message: 'Saving high score...', - backdropDismiss: true, - }) - - await loader.present() + const loader = this.loader.open('Saving high score...').subscribe() try { await this.embassyApi.setDbValue( - ['gaming', 'snake', 'high-score'], + ['gaming', 'snake', 'highScore'], data.highScore, ) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - this.loadingCtrl.dismiss() + this.loader.subscribe() } }) diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html b/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html index e0437cd1d..ad422a8f8 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html @@ -1,20 +1,16 @@

- {{ - hasValidBackup - ? 'Available, contains existing backup' - : 'Available for fresh backup' - }} + Available for backup

-

+

- StartOS backup detected + StartOS backups detected

-

+

- No StartOS backup + No StartOS backups

diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html index 141872084..82e508420 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html @@ -73,7 +73,7 @@

{{ cifs.path.split('/').pop() }}

@@ -155,7 +155,7 @@

{{ drive.label || drive.logicalname }}

{{ drive.vendor || 'Unknown Vendor' }} - diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts index bf7f844e3..60c37d08d 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts @@ -10,7 +10,6 @@ import { UnitConversionPipesModule, TextSpinnerComponentModule, } from '@start9labs/shared' -import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' @NgModule({ declarations: [ @@ -23,7 +22,6 @@ import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form. IonicModule, UnitConversionPipesModule, TextSpinnerComponentModule, - GenericFormPageModule, ], exports: [ BackupDrivesComponent, diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index fa73b8452..859433077 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -1,21 +1,18 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' -import { BackupService } from './backup.service' +import { ActionSheetController, AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { ISB } from '@start9labs/start-sdk' import { CifsBackupTarget, DiskBackupTarget, RR, } from 'src/app/services/api/api.types' -import { - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { FormComponent } from '../form.component' +import { BackupService } from './backup.service' type BackupType = 'create' | 'restore' @@ -32,13 +29,13 @@ export class BackupDrivesComponent { loadingText = '' constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly actionCtrl: ActionSheetController, private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly backupService: BackupService, + private readonly formDialog: FormDialogService, ) {} get loading() { @@ -75,10 +72,10 @@ export class BackupDrivesComponent { return } - if (this.type === 'restore' && !target.hasValidBackup) { + if (this.type === 'restore' && !target.hasAnyBackup) { const message = `${ target.entry.type === 'cifs' ? 'Network Folder' : 'Drive partition' - } does not contain a valid Start9 Server backup.` + } does not contain a valid backup.` this.presentAlertError(message) return } @@ -87,23 +84,19 @@ export class BackupDrivesComponent { } async presentModalAddCifs(): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'New Network Folder', - spec: CifsSpec, + this.formDialog.open(FormComponent, { + label: 'New Network Folder', + data: { + spec: await configBuilderToSpec(cifsSpec), buttons: [ { - text: 'Connect', - handler: (value: RR.AddBackupTargetReq) => { - return this.addCifs(value) - }, - isSubmit: true, + text: 'Execute', + handler: async (value: RR.AddBackupTargetReq) => + this.addCifs(value), }, ], }, }) - await modal.present() } async presentActionCifs( @@ -151,25 +144,24 @@ export class BackupDrivesComponent { } private async addCifs(value: RR.AddBackupTargetReq): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Testing connectivity to shared folder...', - }) - await loader.present() + const loader = this.loader + .open('Testing connectivity to shared folder...') + .subscribe() try { const res = await this.embassyApi.addBackupTarget(value) const [id, entry] = Object.entries(res)[0] this.backupService.cifs.unshift({ id, - hasValidBackup: this.backupService.hasValidBackup(entry), + hasAnyBackup: this.backupService.hasAnyBackup(entry), entry, }) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -180,62 +172,57 @@ export class BackupDrivesComponent { ): Promise { const { hostname, path, username } = entry - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Update Shared Folder', - spec: CifsSpec, + this.formDialog.open(FormComponent, { + label: 'Update Network Folder', + data: { + spec: await configBuilderToSpec(cifsSpec), buttons: [ { - text: 'Save', - handler: (value: RR.AddBackupTargetReq) => { - return this.editCifs({ id, ...value }, index) - }, - isSubmit: true, + text: 'Execute', + handler: async (value: RR.AddBackupTargetReq) => + this.editCifs({ id, ...value }, index), }, ], - initialValue: { + value: { hostname, path, username, }, }, }) - await modal.present() } private async editCifs( value: RR.UpdateBackupTargetReq, index: number, - ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Testing connectivity to shared folder...', - }) - await loader.present() + ): Promise { + const loader = this.loader + .open('Testing connectivity to shared folder...') + .subscribe() try { const res = await this.embassyApi.updateBackupTarget(value) this.backupService.cifs[index].entry = Object.values(res)[0] + + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) + return false } finally { - loader.dismiss() + loader.unsubscribe() } } private async deleteCifs(id: string, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Removing...', - }) - await loader.present() + const loader = this.loader.open('Removing...').subscribe() try { await this.embassyApi.removeBackupTarget({ id }) this.backupService.cifs.splice(index, 1) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -271,43 +258,40 @@ export class BackupDrivesHeaderComponent { }) export class BackupDrivesStatusComponent { @Input() type!: BackupType - @Input() hasValidBackup!: boolean + @Input() hasAnyBackup!: boolean } -const CifsSpec: ConfigSpec = { - hostname: { - type: 'string', - name: 'Hostname/IP', +const cifsSpec = ISB.InputSpec.of({ + hostname: ISB.Value.text({ + name: 'Hostname', description: - 'The hostname or IP address of the target device on your Local Area Network.', - placeholder: `e.g. 'MyComputer.local' OR '192.168.1.4'`, - nullable: false, - masked: false, - copyable: false, - }, - path: { - type: 'string', + 'The hostname of your target device on the Local Area Network.', + warning: null, + placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, + required: true, + default: null, + patterns: [], + }), + path: ISB.Value.text({ name: 'Path', description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', - nullable: false, - masked: false, - copyable: false, - }, - username: { - type: 'string', + required: true, + default: null, + }), + username: ISB.Value.text({ name: 'Username', description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, - nullable: false, - masked: false, - copyable: false, - }, - password: { - type: 'string', + required: true, + default: null, + placeholder: 'My Network Folder', + }), + password: ISB.Value.text({ name: 'Password', description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, - nullable: true, + required: false, + default: null, masked: true, - copyable: false, - }, -} + placeholder: 'My Network Folder', + }), +}) diff --git a/web/projects/ui/src/app/components/backup-drives/backup.service.ts b/web/projects/ui/src/app/components/backup-drives/backup.service.ts index 381110de4..ec42c8266 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -7,7 +7,8 @@ import { DiskBackupTarget, } from 'src/app/services/api/api.types' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' -import { getErrorMessage, Emver } from '@start9labs/shared' +import { getErrorMessage } from '@start9labs/shared' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -18,10 +19,7 @@ export class BackupService { loading = true loadingError: string | IonicSafeString = '' - constructor( - private readonly embassyApi: ApiService, - private readonly emver: Emver, - ) {} + constructor(private readonly embassyApi: ApiService) {} async getBackupTargets(): Promise { this.loading = true @@ -34,7 +32,7 @@ export class BackupService { .map(([id, cifs]) => { return { id, - hasValidBackup: this.hasValidBackup(cifs), + hasAnyBackup: this.hasAnyBackup(cifs), entry: cifs as CifsBackupTarget, } }) @@ -44,7 +42,7 @@ export class BackupService { .map(([id, drive]) => { return { id, - hasValidBackup: this.hasValidBackup(drive), + hasAnyBackup: this.hasAnyBackup(drive), entry: drive as DiskBackupTarget, } }) @@ -55,8 +53,18 @@ export class BackupService { } } - hasValidBackup(target: BackupTarget): boolean { - const backup = target['embassy-os'] - return !!backup && this.emver.compare(backup.version, '0.3.0') !== -1 + hasAnyBackup(target: BackupTarget): boolean { + return Object.values(target.startOs).some( + s => Version.parse(s.version).compare(Version.parse('0.3.6')) !== 'less', + ) + } + + hasThisBackup(target: BackupTarget, id: string): boolean { + return ( + target.startOs[id] && + Version.parse(target.startOs[id].version).compare( + Version.parse('0.3.6'), + ) !== 'less' + ) } } diff --git a/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts b/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts index 3f91da5c4..0d7f3ec66 100644 --- a/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts +++ b/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts @@ -21,8 +21,8 @@ const { enableWidgets } = }) export class BadgeMenuComponent { readonly unreadCount$ = this.patch.watch$( - 'server-info', - 'unread-notification-count', + 'serverInfo', + 'unreadNotificationCount', ) readonly sidebarOpen$ = this.splitPane.sidebarOpen$ readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$ @@ -32,7 +32,7 @@ export class BadgeMenuComponent { constructor( private readonly splitPane: SplitPaneTracker, private readonly patch: PatchDB, - private readonly dialog: TuiDialogService, + private readonly dialogs: TuiDialogService, private readonly clientStorageService: ClientStorageService, ) {} @@ -44,6 +44,6 @@ export class BadgeMenuComponent { } onWidgets() { - this.dialog.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe() + this.dialogs.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe() } } diff --git a/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts b/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts index 9c4b07b7f..cf0eab598 100644 --- a/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts +++ b/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { PatchDB } from 'patch-db-client' import { combineLatest, map, Observable, startWith } from 'rxjs' -import { ConnectionService } from 'src/app/services/connection.service' +import { NetworkService } from 'src/app/services/network.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { StateService } from 'src/app/services/state.service' @Component({ selector: 'connection-bar', @@ -11,19 +12,17 @@ import { DataModel } from 'src/app/services/patch-db/data-model' changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConnectionBarComponent { - private readonly websocket$ = this.connectionService.websocketConnected$ - readonly connection$: Observable<{ message: string color: string icon: string dots: boolean }> = combineLatest([ - this.connectionService.networkConnected$, - this.websocket$.pipe(startWith(false)), + this.network$, + this.state$.pipe(map(Boolean)), this.patch - .watch$('server-info', 'status-info') - .pipe(startWith({ restarting: false, 'shutting-down': false })), + .watch$('serverInfo', 'statusInfo') + .pipe(startWith({ restarting: false, shuttingDown: false })), ]).pipe( map(([network, websocket, status]) => { if (!network) @@ -40,7 +39,7 @@ export class ConnectionBarComponent { icon: 'cloud-offline-outline', dots: true, } - if (status['shutting-down']) + if (status.shuttingDown) return { message: 'Shutting Down', color: 'dark', @@ -65,7 +64,8 @@ export class ConnectionBarComponent { ) constructor( - private readonly connectionService: ConnectionService, + private readonly network$: NetworkService, + private readonly state$: StateService, private readonly patch: PatchDB, ) {} } diff --git a/web/projects/ui/src/app/components/form-object/form-label.component.html b/web/projects/ui/src/app/components/form-object/form-label.component.html deleted file mode 100644 index 69c29f99d..000000000 --- a/web/projects/ui/src/app/components/form-object/form-label.component.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - -{{ data.name }} - - (New) - (New Options) - (Edited) - - * diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.html b/web/projects/ui/src/app/components/form-object/form-object.component.html deleted file mode 100644 index 8bbfc5c78..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.html +++ /dev/null @@ -1,372 +0,0 @@ - -

-
- - - -

- -

- - - - - - - - - - {{ spec.units }} - - -

- - {{ errors | getError: $any(spec)['pattern-description'] }} - -

-
- - - - - - - - {{ spec.name }} - - (New) - - - (Edited) - - - - - - - - - - {{ spec['value-names'][option] }} - - - - - - - - - - - - -
- -
-
-
- - - - - - - - - - - Add - - -

- - {{ errors | getError }} - -

- -
-
- - - - - - - - - - - -
- - - Delete - -
-
-
- -
- - - - - - -

- - {{ errors | getError: $any(spec)['pattern-description'] }} - -

-
-
-
-
-
- - - - -

- -

- - - -

{{ formArr.value | toEnumListDisplay: $any(spec.spec) }}

-
- - - -
-

- - {{ errors | getError }} - -

-
-
-
-
- diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.module.ts b/web/projects/ui/src/app/components/form-object/form-object.component.module.ts deleted file mode 100644 index 06af388c0..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.module.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { - FormObjectComponent, - FormUnionComponent, - FormLabelComponent, -} from './form-object.component' -import { - GetErrorPipe, - ToWarningTextPipe, - ToElementIdPipe, - GetControlPipe, - ToEnumListDisplayPipe, - ToRangePipe, -} from './form-object.pipes' -import { IonicModule } from '@ionic/angular' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { SharedPipesModule } from '@start9labs/shared' -import { TuiElasticContainerModule } from '@taiga-ui/kit' -import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module' -import { TuiExpandModule } from '@taiga-ui/core' - -@NgModule({ - declarations: [ - FormObjectComponent, - FormUnionComponent, - FormLabelComponent, - ToWarningTextPipe, - GetErrorPipe, - ToEnumListDisplayPipe, - ToElementIdPipe, - GetControlPipe, - ToRangePipe, - ], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - SharedPipesModule, - EnumListPageModule, - TuiElasticContainerModule, - TuiExpandModule, - ], - exports: [FormObjectComponent, FormLabelComponent], -}) -export class FormObjectComponentModule {} diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.scss b/web/projects/ui/src/app/components/form-object/form-object.component.scss deleted file mode 100644 index a98a0ff72..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -.slot-start { - display: inline-block; - vertical-align: middle; - --padding-start: 0; - --padding-end: 7px; -} - -.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); -} - -.redacted { - font-family: 'Redacted' -} - -ion-input { - font-family: 'Courier New'; - font-weight: bold; - --placeholder-font-weight: 400; -} - -ion-item-divider { - text-transform: unset; - --padding-top: 18px; - --padding-start: 0; - border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13)))) -} - -.nested-wrapper { - padding: 0 0 16px 24px; -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} - -.indent { - margin-left: 24px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.ts b/web/projects/ui/src/app/components/form-object/form-object.component.ts deleted file mode 100644 index fcc6a0c61..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ChangeDetectionStrategy, - Inject, - inject, - SimpleChanges, -} from '@angular/core' -import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms' -import { AlertButton, AlertController, ModalController } from '@ionic/angular' -import { - ConfigSpec, - ListValueSpecOf, - ValueSpec, - ValueSpecBoolean, - ValueSpecEnum, - ValueSpecList, - ValueSpecListOf, - ValueSpecUnion, -} from 'src/app/pkg-config/config-types' -import { FormService } from 'src/app/services/form.service' -import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page' -import { THEME, pauseFor } from '@start9labs/shared' -import { v4 } from 'uuid' -import { DOCUMENT } from '@angular/common' - -const Mustache = require('mustache') - -interface Config { - [key: string]: any -} -@Component({ - selector: 'form-object', - templateUrl: './form-object.component.html', - styleUrls: ['./form-object.component.scss'], -}) -export class FormObjectComponent { - @Input() objectSpec!: ConfigSpec - @Input() formGroup!: UntypedFormGroup - @Input() current?: Config - @Input() original?: Config - @Output() onInputChange = new EventEmitter() - @Output() hasNewOptions = new EventEmitter() - warningAck: { [key: string]: boolean } = {} - unmasked: { [key: string]: boolean } = {} - objectDisplay: { - [key: string]: { expanded: boolean; hasNewOptions: boolean } - } = {} - objectListDisplay: { - [key: string]: { expanded: boolean; displayAs: string }[] - } = {} - objectId = v4() - - readonly theme$ = inject(THEME) - - constructor( - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - @Inject(DOCUMENT) private readonly document: Document, - ) {} - - ngOnInit() { - this.setDisplays() - - // setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - if ( - this.original && - Object.keys(this.current || {}).some( - key => this.original![key] === undefined, - ) - ) - this.hasNewOptions.emit() - }) - } - - ngOnChanges(changes: SimpleChanges) { - const specChanges = changes['objectSpec'] - - if (!specChanges) return - - if ( - !specChanges.firstChange && - Object.keys({ - ...specChanges.previousValue, - ...specChanges.currentValue, - }).length !== Object.keys(specChanges.previousValue).length - ) { - this.setDisplays() - } - } - - private setDisplays() { - Object.keys(this.objectSpec).forEach(key => { - const spec = this.objectSpec[key] - - if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) { - this.objectListDisplay[key] = [] - this.formGroup.get(key)?.value.forEach((obj: any, index: number) => { - const displayAs = (spec.spec as ListValueSpecOf<'object'>)[ - 'display-as' - ] - this.objectListDisplay[key][index] = { - expanded: false, - displayAs: displayAs - ? (Mustache as any).render(displayAs, obj) - : '', - } - }) - } else if (spec.type === 'object') { - this.objectDisplay[key] = { - expanded: false, - hasNewOptions: false, - } - } - }) - } - - addListItemWrapper( - key: string, - spec: T extends ValueSpecUnion ? never : T, - ) { - this.presentAlertChangeWarning(key, spec, () => this.addListItem(key)) - } - - toggleExpandObject(key: string) { - this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded - } - - toggleExpandListObject(key: string, i: number) { - this.objectListDisplay[key][i].expanded = - !this.objectListDisplay[key][i].expanded - } - - updateLabel(key: string, i: number, displayAs: string) { - this.objectListDisplay[key][i].displayAs = displayAs - ? Mustache.render(displayAs, this.formGroup.get(key)?.value[i]) - : '' - } - - handleInputChange() { - this.onInputChange.emit() - } - - setHasNew(key: string) { - this.hasNewOptions.emit() - setTimeout(() => { - this.objectDisplay[key].hasNewOptions = true - }, 100) - } - - handleBooleanChange(key: string, spec: ValueSpecBoolean) { - if (spec.warning) { - const current = this.formGroup.get(key)?.value - const cancelFn = () => this.formGroup.get(key)?.setValue(!current) - this.presentAlertChangeWarning(key, spec, undefined, cancelFn) - } - } - - async presentModalEnumList( - key: string, - spec: ValueSpecListOf<'enum'>, - current: string[], - ) { - const modal = await this.modalCtrl.create({ - componentProps: { - key, - spec, - current, - }, - component: EnumListPage, - }) - - modal.onWillDismiss().then(({ data }) => { - if (!data) return - this.updateEnumList(key, current, data) - }) - - await modal.present() - } - - async presentAlertChangeWarning( - key: string, - spec: T extends ValueSpecUnion ? never : T, - okFn?: Function, - cancelFn?: Function, - ) { - if (!spec.warning || this.warningAck[key]) return okFn ? okFn() : null - this.warningAck[key] = true - - const buttons: AlertButton[] = [ - { - text: 'Ok', - handler: () => { - if (okFn) okFn() - }, - cssClass: 'enter-click', - }, - ] - - if (okFn || cancelFn) { - buttons.unshift({ - text: 'Cancel', - handler: () => { - if (cancelFn) cancelFn() - }, - }) - } - - const alert = await this.alertCtrl.create({ - header: 'Warning', - subHeader: `Editing ${spec.name} has consequences:`, - message: spec.warning, - buttons, - }) - await alert.present() - } - - async presentAlertDelete(key: string, index: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Are you sure you want to delete this entry?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.deleteListItem(key, index) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async presentAlertBoolEnumDescription( - event: Event, - spec: ValueSpecBoolean | ValueSpecEnum, - ) { - event.stopPropagation() - const { name, description } = spec - - const alert = await this.alertCtrl.create({ - header: name, - message: description, - buttons: [ - { - text: 'OK', - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - private addListItem(key: string): void { - const arr = this.formGroup.get(key) as UntypedFormArray - const listSpec = this.objectSpec[key] as ValueSpecList - const newItem = this.formService.getListItem(listSpec, undefined)! - - const index = arr.length - arr.insert(index, newItem) - - if (['object', 'union'].includes(listSpec.subtype)) { - const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[ - 'display-as' - ] - this.objectListDisplay[key].push({ - expanded: false, - displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '', - }) - } - - setTimeout(() => { - const element = this.document.getElementById( - getElementId(this.objectId, key, index), - ) - element?.parentElement?.scrollIntoView({ behavior: 'smooth' }) - - if (['object', 'union'].includes(listSpec.subtype)) { - pauseFor(250).then(() => this.toggleExpandListObject(key, index)) - } - }, 100) - - arr.markAsDirty() - } - - private deleteListItem(key: string, index: number, markDirty = true): void { - // if (this.objectListDisplay[key]) - // this.objectListDisplay[key][index].height = '0px' - const arr = this.formGroup.get(key) as UntypedFormArray - if (markDirty) arr.markAsDirty() - pauseFor(250).then(() => { - if (this.objectListDisplay[key]) - this.objectListDisplay[key].splice(index, 1) - arr.removeAt(index) - }) - } - - private updateEnumList(key: string, current: string[], updated: string[]) { - const arr = this.formGroup.get(key) as FormArray - - for (let i = current.length - 1; i >= 0; i--) { - if (!updated.includes(current[i])) { - arr.removeAt(i) - } - } - - const listSpec = this.objectSpec[key] as ValueSpecList - - updated.forEach(val => { - if (!current.includes(val)) { - const newItem = this.formService.getListItem(listSpec, val)! - arr.insert(arr.length, newItem) - } - }) - - arr.markAsDirty() - } - - asIsOrder() { - return 0 - } -} - -@Component({ - selector: 'form-union', - templateUrl: './form-union.component.html', - styleUrls: ['./form-object.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormUnionComponent { - @Input() formGroup!: UntypedFormGroup - @Input() spec!: ValueSpecUnion - @Input() current?: Config - @Input() original?: Config - - get unionValue() { - return this.formGroup.get(this.spec.tag.id)?.value - } - - get isNew() { - return !this.original - } - - get hasNewOptions() { - const tagId = this.spec.tag.id - return ( - this.original?.[tagId] === this.current?.[tagId] && - !!Object.keys(this.current || {}).find( - key => this.original![key] === undefined, - ) - ) - } - - objectId = v4() - - constructor(private readonly formService: FormService) {} - - updateUnion(e: any): void { - const tagId = this.spec.tag.id - - Object.keys(this.formGroup.controls).forEach(control => { - if (control === tagId) return - this.formGroup.removeControl(control) - }) - - const unionGroup = this.formService.getUnionObject( - this.spec as ValueSpecUnion, - e.detail.value, - ) - - Object.keys(unionGroup.controls).forEach(control => { - if (control === tagId) return - this.formGroup.addControl(control, unionGroup.controls[control]) - }) - } -} - -@Component({ - selector: 'form-label', - templateUrl: './form-label.component.html', - styleUrls: ['./form-object.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormLabelComponent { - @Input() data!: { - name: string - new: boolean - edited: boolean - description?: string - required?: boolean - newOptions?: boolean - } - - constructor(private readonly alertCtrl: AlertController) {} - - async presentAlertDescription(event: Event) { - event.stopPropagation() - const { name, description } = this.data - - const alert = await this.alertCtrl.create({ - header: name, - message: description, - buttons: [ - { - text: 'OK', - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } -} - -export function getElementId(objectId: string, key: string, index = 0): string { - return `${key}-${index}-${objectId}` -} diff --git a/web/projects/ui/src/app/components/form-object/form-object.pipes.ts b/web/projects/ui/src/app/components/form-object/form-object.pipes.ts deleted file mode 100644 index 1dc5a18f2..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.pipes.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { - AbstractControl, - FormGroup, - UntypedFormArray, - ValidationErrors, -} from '@angular/forms' -import { IonicSafeString } from '@ionic/angular' -import { ListValueSpecOf } from 'src/app/pkg-config/config-types' -import { Range } from 'src/app/pkg-config/config-utilities' -import { getElementId } from './form-object.component' - -@Pipe({ - name: 'getError', -}) -export class GetErrorPipe implements PipeTransform { - transform(errors: ValidationErrors, patternDesc?: string): string { - if (errors['required']) { - return 'Required' - } else if (errors['pattern']) { - return patternDesc || 'Invalid pattern' - } else if (errors['notNumber']) { - return 'Must be a number' - } else if (errors['numberNotInteger']) { - return 'Must be an integer' - } else if (errors['numberNotInRange']) { - return errors['numberNotInRange'].value - } else if (errors['listNotUnique']) { - return errors['listNotUnique'].value - } else if (errors['listNotInRange']) { - return errors['listNotInRange'].value - } else if (errors['listItemIssue']) { - return errors['listItemIssue'].value - } else { - return 'Unknown error' - } - } -} - -@Pipe({ - name: 'toEnumListDisplay', -}) -export class ToEnumListDisplayPipe implements PipeTransform { - transform(arr: string[], spec: ListValueSpecOf<'enum'>): string { - return arr.map((v: string) => spec['value-names'][v]).join(', ') - } -} - -@Pipe({ - name: 'toWarningText', -}) -export class ToWarningTextPipe implements PipeTransform { - transform(text?: string): IonicSafeString | string { - return text - ? new IonicSafeString(`${text}`) - : '' - } -} - -@Pipe({ - name: 'toRange', -}) -export class ToRangePipe implements PipeTransform { - transform(range: string): Range { - return Range.from(range) - } -} - -@Pipe({ - name: 'toElementId', -}) -export class ToElementIdPipe implements PipeTransform { - transform(objectId: string, key: string, index = 0): string { - return getElementId(objectId, key, index) - } -} - -@Pipe({ - name: 'getControl', -}) -export class GetControlPipe implements PipeTransform { - transform( - formGroup: FormGroup, - key: string, - index?: number, - ): AbstractControl { - const abstractControl = formGroup.get(key)! - if (index !== undefined) - return (abstractControl as UntypedFormArray).at(index) - return abstractControl - } -} diff --git a/web/projects/ui/src/app/components/form-object/form-union.component.html b/web/projects/ui/src/app/components/form-object/form-union.component.html deleted file mode 100644 index ed9fc31be..000000000 --- a/web/projects/ui/src/app/components/form-object/form-union.component.html +++ /dev/null @@ -1,42 +0,0 @@ -
- - - - - - - {{ spec.tag['variant-names'][option.key] }} - - - - - - - -
diff --git a/web/projects/ui/src/app/components/form.component.ts b/web/projects/ui/src/app/components/form.component.ts new file mode 100644 index 000000000..e8f8b3f76 --- /dev/null +++ b/web/projects/ui/src/app/components/form.component.ts @@ -0,0 +1,164 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + OnInit, +} from '@angular/core' +import { FormGroup, ReactiveFormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' +import { IST } from '@start9labs/start-sdk' +import { + tuiMarkControlAsTouchedAndValidate, + TuiValueChangesModule, +} from '@taiga-ui/cdk' +import { TuiDialogContext, TuiModeModule } from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiDialogFormService } from '@taiga-ui/kit' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { compare, Operation } from 'fast-json-patch' +import { FormModule } from 'src/app/components/form/form.module' +import { InvalidService } from 'src/app/components/form/invalid.service' +import { FormService } from 'src/app/services/form.service' + +export interface ActionButton { + text: string + handler?: (value: T) => Promise | void + link?: string +} + +export interface FormContext { + spec: IST.InputSpec + buttons: ActionButton[] + value?: T + operations?: Operation[] +} + +@Component({ + standalone: true, + selector: 'app-form', + template: ` +
+ + +
+ `, + styles: [ + ` + footer { + position: sticky; + bottom: 0; + z-index: 10; + display: flex; + justify-content: flex-end; + padding: 1rem 0; + margin: 1rem 0 -1rem; + gap: 1rem; + background: var(--tui-elevation-01); + border-top: 1px solid var(--tui-base-02); + } + `, + ], + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule, + TuiValueChangesModule, + TuiButtonModule, + TuiModeModule, + FormModule, + ], + providers: [InvalidService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormComponent> implements OnInit { + private readonly dialogFormService = inject(TuiDialogFormService) + private readonly formService = inject(FormService) + private readonly invalidService = inject(InvalidService) + private readonly context = inject>>( + POLYMORPHEUS_CONTEXT, + { optional: true }, + ) + + @Input() spec = this.context?.data.spec || {} + @Input() buttons = this.context?.data.buttons || [] + @Input() operations = this.context?.data.operations || [] + @Input() value?: T = this.context?.data.value + + form = new FormGroup({}) + + ngOnInit() { + this.dialogFormService.markAsPristine() + this.form = this.formService.createForm(this.spec, this.value) + this.process(this.operations) + } + + onReset() { + this.form = this.formService.createForm(this.spec) + tuiMarkControlAsTouchedAndValidate(this.form) + this.markAsDirty() + } + + async onClick(handler: Required>['handler']) { + tuiMarkControlAsTouchedAndValidate(this.form) + this.invalidService.scrollIntoView() + + if (this.form.valid && (await handler(this.form.value as T))) { + this.close() + } + } + + markAsDirty() { + this.dialogFormService.markAsDirty() + } + + close() { + this.context?.$implicit.complete() + } + + private process(operations: Operation[]) { + operations.forEach(operation => { + const control = this.form.get(operation.path.substring(1).split('/')) + + if (!control || !control.parent) return + + if (operation.op === 'add' || operation.op === 'replace') { + control.markAsDirty() + control.markAsTouched() + control.setValue(operation.value) + } + + control.parent.markAsDirty() + control.parent.markAsTouched() + }) + } +} diff --git a/web/projects/ui/src/app/components/form/control.directive.ts b/web/projects/ui/src/app/components/form/control.directive.ts new file mode 100644 index 000000000..327eb8255 --- /dev/null +++ b/web/projects/ui/src/app/components/form/control.directive.ts @@ -0,0 +1,30 @@ +import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core' +import { ControlContainer, NgControl } from '@angular/forms' +import { InvalidService } from './invalid.service' + +@Directive({ + selector: 'form-control, form-array, form-object', +}) +export class ControlDirective implements OnInit, OnDestroy { + private readonly invalidService = inject(InvalidService, { optional: true }) + private readonly element: ElementRef = inject(ElementRef) + private readonly control = + inject(NgControl, { optional: true }) || + inject(ControlContainer, { optional: true }) + + get invalid(): boolean { + return !!this.control?.invalid + } + + scrollIntoView() { + this.element.nativeElement.scrollIntoView({ behavior: 'smooth' }) + } + + ngOnInit() { + this.invalidService?.add(this) + } + + ngOnDestroy() { + this.invalidService?.remove(this) + } +} diff --git a/web/projects/ui/src/app/components/form/control.ts b/web/projects/ui/src/app/components/form/control.ts new file mode 100644 index 000000000..5cf9d84ab --- /dev/null +++ b/web/projects/ui/src/app/components/form/control.ts @@ -0,0 +1,38 @@ +import { inject } from '@angular/core' +import { FormControlComponent } from './form-control/form-control.component' +import { IST } from '@start9labs/start-sdk' + +export abstract class Control< + Spec extends Exclude, + Value, +> { + private readonly control: FormControlComponent = + inject(FormControlComponent) + + get invalid(): boolean { + return this.control.touched && this.control.invalid + } + + get spec(): Spec { + return this.control.spec + } + + // TODO: Properly handle already set immutable value + get readOnly(): boolean { + return ( + !!this.value && !!this.control.control?.pristine && this.control.immutable + ) + } + + get value(): Value | null { + return this.control.value + } + + set value(value: Value | null) { + this.control.onInput(value) + } + + onFocus(focused: boolean) { + this.control.onFocus(focused) + } +} diff --git a/web/projects/ui/src/app/components/form/filter-hidden.pipe.ts b/web/projects/ui/src/app/components/form/filter-hidden.pipe.ts new file mode 100644 index 000000000..84666a2c9 --- /dev/null +++ b/web/projects/ui/src/app/components/form/filter-hidden.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { KeyValue } from '@angular/common' + +@Pipe({ + name: 'filterHidden', +}) +export class FilterHiddenPipe implements PipeTransform { + transform(value: KeyValue[]) { + return value.filter(x => x.value.type !== 'hidden') as KeyValue< + string, + Exclude + >[] + } +} diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/components/form/form-array/form-array.component.html new file mode 100644 index 000000000..d66387fb5 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.html @@ -0,0 +1,58 @@ +
+ {{ spec.name }} + + +
+ + + + + {{ item.value | mustache : $any(spec.spec).displayAs }} + + + + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.scss b/web/projects/ui/src/app/components/form/form-array/form-array.component.scss new file mode 100644 index 000000000..9b6415ff7 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.scss @@ -0,0 +1,50 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + display: block; + margin: 2rem 0; +} + +.label { + display: flex; + font-size: 1.25rem; + font-weight: bold; +} + +.add { + font-size: 1rem; + padding: 0 1rem; + margin-left: auto; +} + +.object { + display: block; + position: relative; + + &_open::after, + &:last-child::after { + opacity: 0; + } + + &:after { + @include transition(opacity); + + content: ''; + position: absolute; + bottom: -0.5rem; + height: 1px; + left: 3rem; + right: 1rem; + background: var(--tui-clear); + } +} + +.remove { + margin-left: auto; + pointer-events: auto; +} + +.control { + display: block; + margin: 0.5rem 0; +} diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.ts b/web/projects/ui/src/app/components/form/form-array/form-array.component.ts new file mode 100644 index 000000000..25242f826 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.ts @@ -0,0 +1,91 @@ +import { Component, HostBinding, inject, Input } from '@angular/core' +import { AbstractControl, FormArrayName } from '@angular/forms' +import { TUI_PARENT_ANIMATION, TuiDestroyService } from '@taiga-ui/cdk' +import { + TUI_ANIMATION_OPTIONS, + TuiDialogService, + tuiFadeIn, + tuiHeightCollapse, +} from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { IST } from '@start9labs/start-sdk' +import { filter, takeUntil } from 'rxjs' +import { FormService } from 'src/app/services/form.service' +import { ERRORS } from '../form-group/form-group.component' + +@Component({ + selector: 'form-array', + templateUrl: './form-array.component.html', + styleUrls: ['./form-array.component.scss'], + animations: [tuiFadeIn, tuiHeightCollapse, TUI_PARENT_ANIMATION], + providers: [TuiDestroyService], +}) +export class FormArrayComponent { + @Input() + spec!: IST.ValueSpecList + + @HostBinding('@tuiParentAnimation') + readonly animation = { value: '', ...inject(TUI_ANIMATION_OPTIONS) } + readonly order = ERRORS + readonly array = inject(FormArrayName) + readonly open = new Map() + + private warned = false + private readonly formService = inject(FormService) + private readonly dialogs = inject(TuiDialogService) + private readonly destroy$ = inject(TuiDestroyService) + + get canAdd(): boolean { + return ( + !this.spec.disabled && + (!this.spec.maxLength || + this.spec.maxLength >= this.array.control.controls.length) + ) + } + + add() { + if (!this.warned && this.spec.warning) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { content: this.spec.warning, yes: 'Ok', no: 'Cancel' }, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.addItem() + }) + } else { + this.addItem() + } + + this.warned = true + } + + removeAt(index: number) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Are you sure you want to delete this entry?', + yes: 'Delete', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.removeItem(index) + }) + } + + private removeItem(index: number) { + this.open.delete(this.array.control.at(index)) + this.array.control.removeAt(index) + } + + private addItem() { + this.array.control.insert(0, this.formService.getListItem(this.spec)) + this.open.set(this.array.control.at(0), true) + } +} diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.html b/web/projects/ui/src/app/components/form/form-color/form-color.component.html new file mode 100644 index 000000000..19cf4051d --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.html @@ -0,0 +1,31 @@ + + {{ spec.name }} + * + + +
+ + +
+
diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.scss b/web/projects/ui/src/app/components/form/form-color/form-color.component.scss new file mode 100644 index 000000000..49496946e --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.scss @@ -0,0 +1,33 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +.wrapper { + position: relative; + width: 1.5rem; + height: 1.5rem; + pointer-events: auto; + + &::after { + content: ''; + position: absolute; + height: 0.3rem; + width: 1.4rem; + bottom: 0.125rem; + background: currentColor; + border-radius: 0.125rem; + pointer-events: none; + } +} + +.color { + @include fullsize(); + opacity: 0; +} + +.icon { + @include fullsize(); + pointer-events: none; + + input:hover + & { + opacity: 1; + } +} diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.ts b/web/projects/ui/src/app/components/form/form-color/form-color.component.ts new file mode 100644 index 000000000..0f65f06ce --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' +import { MaskitoOptions } from '@maskito/core' + +@Component({ + selector: 'form-color', + templateUrl: './form-color.component.html', + styleUrls: ['./form-color.component.scss'], +}) +export class FormColorComponent extends Control { + readonly mask: MaskitoOptions = { + mask: ['#', ...Array(6).fill(/[0-9a-f]/i)], + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.html b/web/projects/ui/src/app/components/form/form-control/form-control.component.html new file mode 100644 index 000000000..731d64a63 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + {{ spec.warning }} +

This value cannot be changed once set!

+
+ + +
+
diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.scss b/web/projects/ui/src/app/components/form/form-control/form-control.component.scss new file mode 100644 index 000000000..844651118 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; +} + +.buttons { + margin-top: 0.5rem; + + :first-child { + margin-right: 0.5rem; + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.ts b/web/projects/ui/src/app/components/form/form-control/form-control.component.ts new file mode 100644 index 000000000..9544188a6 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { AbstractTuiNullableControl } from '@taiga-ui/cdk' +import { + TuiAlertService, + TuiDialogContext, + TuiNotification, +} from '@taiga-ui/core' +import { filter, takeUntil } from 'rxjs' +import { IST } from '@start9labs/start-sdk' +import { ERRORS } from '../form-group/form-group.component' +import { FORM_CONTROL_PROVIDERS } from './form-control.providers' + +@Component({ + selector: 'form-control', + templateUrl: './form-control.component.html', + styleUrls: ['./form-control.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: FORM_CONTROL_PROVIDERS, +}) +export class FormControlComponent< + T extends Exclude, + V, +> extends AbstractTuiNullableControl { + @Input() + spec!: T + + @ViewChild('warning') + warning?: TemplateRef> + + warned = false + focused = false + readonly order = ERRORS + private readonly alerts = inject(TuiAlertService) + + get immutable(): boolean { + return 'immutable' in this.spec && this.spec.immutable + } + + onFocus(focused: boolean) { + this.focused = focused + this.updateFocused(focused) + } + + onInput(value: V | null) { + const previous = this.value + + if (!this.warned && this.warning) { + this.alerts + .open(this.warning, { + label: 'Warning', + status: TuiNotification.Warning, + hasCloseButton: false, + autoClose: false, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.value = previous + }) + } + + this.warned = true + this.value = value === '' ? null : value + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts b/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts new file mode 100644 index 000000000..f61ac092d --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts @@ -0,0 +1,30 @@ +import { forwardRef, Provider } from '@angular/core' +import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' +import { IST } from '@start9labs/start-sdk' +import { FormControlComponent } from './form-control.component' + +interface ValidatorsPatternError { + actualValue: string + requiredPattern: string | RegExp +} + +export const FORM_CONTROL_PROVIDERS: Provider[] = [ + { + provide: TUI_VALIDATION_ERRORS, + deps: [forwardRef(() => FormControlComponent)], + useFactory: ( + control: FormControlComponent< + Exclude, + string + >, + ) => ({ + required: 'Required', + pattern: ({ requiredPattern }: ValidatorsPatternError) => + ('patterns' in control.spec && + control.spec.patterns.find( + ({ regex }) => String(regex) === String(requiredPattern), + )?.description) || + 'Invalid format', + }), + }, +] diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html new file mode 100644 index 000000000..37387a338 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html @@ -0,0 +1,43 @@ + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts new file mode 100644 index 000000000..fc3acecd0 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core' +import { + TUI_FIRST_DAY, + TUI_LAST_DAY, + TuiDay, + tuiPure, + TuiTime, +} from '@taiga-ui/cdk' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-datetime', + templateUrl: './form-datetime.component.html', +}) +export class FormDatetimeComponent extends Control< + IST.ValueSpecDatetime, + string +> { + readonly min = TUI_FIRST_DAY + readonly max = TUI_LAST_DAY + + @tuiPure + getTime(value: string | null) { + return value ? TuiTime.fromString(value) : null + } + + getLimit(limit: string): [TuiDay, TuiTime] { + return [ + TuiDay.jsonParse(limit.slice(0, 10)), + limit.length === 10 + ? new TuiTime(0, 0) + : TuiTime.fromString(limit.slice(-5)), + ] + } +} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.html b/web/projects/ui/src/app/components/form/form-group/form-group.component.html new file mode 100644 index 000000000..65975e970 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.html @@ -0,0 +1,30 @@ + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.scss b/web/projects/ui/src/app/components/form/form-group/form-group.component.scss new file mode 100644 index 000000000..ce5665fc0 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.scss @@ -0,0 +1,35 @@ +form-group .g-form-control:not(:first-child) { + margin-top: 1rem; +} + +form-group .g-form-group { + position: relative; + padding-left: var(--tui-height-m); + + &::before, + &::after { + content: ''; + position: absolute; + background: var(--tui-clear); + } + + &::before { + top: 0; + left: calc(1rem - 1px); + bottom: 0.5rem; + width: 2px; + } + + &::after { + left: 0.75rem; + bottom: 0; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + } +} + +form-group tui-tooltip { + z-index: 1; + margin-left: 0.25rem; +} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.ts b/web/projects/ui/src/app/components/form/form-group/form-group.component.ts new file mode 100644 index 000000000..456ab1383 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.ts @@ -0,0 +1,35 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + ViewEncapsulation, +} from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { FORM_GROUP_PROVIDERS } from './form-group.providers' + +export const ERRORS = [ + 'required', + 'pattern', + 'notNumber', + 'numberNotInteger', + 'numberNotInRange', + 'listNotUnique', + 'listNotInRange', + 'listItemIssue', +] + +@Component({ + selector: 'form-group', + templateUrl: './form-group.component.html', + styleUrls: ['./form-group.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [FORM_GROUP_PROVIDERS], +}) +export class FormGroupComponent { + @Input() spec: IST.InputSpec = {} + + asIsOrder() { + return 0 + } +} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts b/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts new file mode 100644 index 000000000..5c9039f40 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts @@ -0,0 +1,34 @@ +import { Provider, SkipSelf } from '@angular/core' +import { + TUI_ARROW_MODE, + tuiInputDateOptionsProvider, + tuiInputTimeOptionsProvider, +} from '@taiga-ui/kit' +import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core' +import { ControlContainer } from '@angular/forms' +import { identity, of } from 'rxjs' + +export const FORM_GROUP_PROVIDERS: Provider[] = [ + { + provide: TUI_DEFAULT_ERROR_MESSAGE, + useValue: of('Unknown error'), + }, + { + provide: ControlContainer, + deps: [[new SkipSelf(), ControlContainer]], + useFactory: identity, + }, + { + provide: TUI_ARROW_MODE, + useValue: { + interactive: null, + disabled: null, + }, + }, + tuiInputDateOptionsProvider({ + nativePicker: true, + }), + tuiInputTimeOptionsProvider({ + nativePicker: true, + }), +] diff --git a/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html new file mode 100644 index 000000000..0e2a47cc2 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html @@ -0,0 +1,18 @@ + + {{ spec.name }} + + diff --git a/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts new file mode 100644 index 000000000..9056e1e0f --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' +import { tuiPure } from '@taiga-ui/cdk' +import { invert } from '@start9labs/shared' + +@Component({ + selector: 'form-multiselect', + templateUrl: './form-multiselect.component.html', +}) +export class FormMultiselectComponent extends Control< + IST.ValueSpecMultiselect, + readonly string[] +> { + private readonly inverted = invert(this.spec.values) + + private readonly isDisabled = (item: string) => + Array.isArray(this.spec.disabled) && + this.spec.disabled.includes(this.inverted[item]) + + private readonly isExceedingLimit = (item: string) => + !!this.spec.maxLength && + this.selected.length >= this.spec.maxLength && + !this.selected.includes(item) + + readonly disabledItemHandler = (item: string): boolean => + this.isDisabled(item) || this.isExceedingLimit(item) + + readonly items = Object.values(this.spec.values) + + get disabled(): boolean { + return typeof this.spec.disabled === 'string' + } + + get selected(): string[] { + return this.memoize(this.value) + } + + set selected(value: string[]) { + this.value = Object.entries(this.spec.values) + .filter(([_, v]) => value.includes(v)) + .map(([k]) => k) + } + + @tuiPure + private memoize(value: null | readonly string[]): string[] { + return value?.map(key => this.spec.values[key]) || [] + } +} diff --git a/web/projects/ui/src/app/components/form/form-number/form-number.component.html b/web/projects/ui/src/app/components/form/form-number/form-number.component.html new file mode 100644 index 000000000..c205b2bb8 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-number/form-number.component.html @@ -0,0 +1,18 @@ + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-number/form-number.component.ts b/web/projects/ui/src/app/components/form/form-number/form-number.component.ts new file mode 100644 index 000000000..b07858207 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-number/form-number.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-number', + templateUrl: './form-number.component.html', +}) +export class FormNumberComponent extends Control { + protected readonly Infinity = Infinity +} diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.html b/web/projects/ui/src/app/components/form/form-object/form-object.component.html new file mode 100644 index 000000000..589019c15 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.html @@ -0,0 +1,25 @@ +

+ + + {{ spec.name }} + +

+ + +
+ +
+
diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.scss b/web/projects/ui/src/app/components/form/form-object/form-object.component.scss new file mode 100644 index 000000000..c167c89fa --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.scss @@ -0,0 +1,41 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.title { + position: relative; + height: var(--tui-height-l); + display: flex; + align-items: center; + cursor: pointer; + font: var(--tui-font-text-l); + font-weight: bold; + margin: 0 0 -0.75rem; +} + +.button { + @include transition(transform); + + margin-right: 1rem; + + &_open { + transform: rotate(180deg); + } +} + +.expand { + align-self: stretch; +} + +.g-form-group { + padding-top: 0.75rem; + + &_invalid::before, + &_invalid::after { + background: var(--tui-error-bg); + } +} diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.ts b/web/projects/ui/src/app/components/form/form-object/form-object.component.ts new file mode 100644 index 000000000..a036c1e43 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.ts @@ -0,0 +1,38 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + Input, + Output, +} from '@angular/core' +import { ControlContainer } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' + +@Component({ + selector: 'form-object', + templateUrl: './form-object.component.html', + styleUrls: ['./form-object.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormObjectComponent { + @Input() + spec!: IST.ValueSpecObject + + @Input() + open = false + + @Output() + readonly openChange = new EventEmitter() + + private readonly container = inject(ControlContainer) + + get invalid() { + return !this.container.valid && this.container.touched + } + + toggle() { + this.open = !this.open + this.openChange.emit(this.open) + } +} diff --git a/web/projects/ui/src/app/components/form/form-select/form-select.component.html b/web/projects/ui/src/app/components/form/form-select/form-select.component.html new file mode 100644 index 000000000..b8e79351a --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-select/form-select.component.html @@ -0,0 +1,17 @@ + + {{ spec.name }} * + + diff --git a/web/projects/ui/src/app/components/form/form-select/form-select.component.ts b/web/projects/ui/src/app/components/form/form-select/form-select.component.ts new file mode 100644 index 000000000..ac478a4d1 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-select/form-select.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { invert } from '@start9labs/shared' +import { Control } from '../control' + +@Component({ + selector: 'form-select', + templateUrl: './form-select.component.html', +}) +export class FormSelectComponent extends Control { + private readonly inverted = invert(this.spec.values) + + readonly items = Object.values(this.spec.values) + + readonly disabledItemHandler = (item: string) => + Array.isArray(this.spec.disabled) && + this.spec.disabled.includes(this.inverted[item]) + + get disabled(): boolean { + return typeof this.spec.disabled === 'string' + } + + get selected(): string | null { + return (this.value && this.spec.values[this.value]) || null + } + + set selected(value: string | null) { + this.value = (value && this.inverted[value]) || null + } +} diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.html b/web/projects/ui/src/app/components/form/form-text/form-text.component.html new file mode 100644 index 000000000..0db900238 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.html @@ -0,0 +1,44 @@ + + {{ spec.name }} + * + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.scss b/web/projects/ui/src/app/components/form/form-text/form-text.component.scss new file mode 100644 index 000000000..05e47885b --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.scss @@ -0,0 +1,8 @@ +.button { + pointer-events: auto; + margin-left: 0.25rem; +} + +.masked { + -webkit-text-security: disc; +} diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.ts b/web/projects/ui/src/app/components/form/form-text/form-text.component.ts new file mode 100644 index 000000000..703570dbc --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core' +import { IST, utils } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-text', + templateUrl: './form-text.component.html', + styleUrls: ['./form-text.component.scss'], +}) +export class FormTextComponent extends Control { + masked = true + + generate() { + this.value = utils.getDefaultString(this.spec.generate || '') + } +} diff --git a/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html new file mode 100644 index 000000000..1be4a67a2 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html @@ -0,0 +1,15 @@ + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts new file mode 100644 index 000000000..0c2bd054d --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-textarea', + templateUrl: './form-textarea.component.html', +}) +export class FormTextareaComponent extends Control< + IST.ValueSpecTextarea, + string +> {} diff --git a/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html new file mode 100644 index 000000000..08a33afe4 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html @@ -0,0 +1,12 @@ +{{ spec.name }} + + diff --git a/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts new file mode 100644 index 000000000..2295bbc2f --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-toggle', + templateUrl: './form-toggle.component.html', + host: { style: 'display: flex' }, +}) +export class FormToggleComponent extends Control< + IST.ValueSpecToggle, + boolean +> {} diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.html b/web/projects/ui/src/app/components/form/form-union/form-union.component.html new file mode 100644 index 000000000..1cb5bfe57 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.html @@ -0,0 +1,11 @@ + + + + diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.scss b/web/projects/ui/src/app/components/form/form-union/form-union.component.scss new file mode 100644 index 000000000..cfb2f95e8 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.scss @@ -0,0 +1,8 @@ +:host { + display: block; +} + +.group { + display: block; + margin-top: 1rem; +} diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.ts b/web/projects/ui/src/app/components/form/form-union/form-union.component.ts new file mode 100644 index 000000000..d1a3b61c1 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.ts @@ -0,0 +1,59 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + OnChanges, +} from '@angular/core' +import { ControlContainer, FormGroupName } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { FormService } from 'src/app/services/form.service' +import { tuiPure } from '@taiga-ui/cdk' + +@Component({ + selector: 'form-union', + templateUrl: './form-union.component.html', + styleUrls: ['./form-union.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: ControlContainer, + useExisting: FormGroupName, + }, + ], +}) +export class FormUnionComponent implements OnChanges { + @Input() + spec!: IST.ValueSpecUnion + + selectSpec!: IST.ValueSpecSelect + + private readonly form = inject(FormGroupName) + private readonly formService = inject(FormService) + private readonly values: Record = {} + + get union(): string { + return this.form.value.selection + } + + @tuiPure + onUnion(union: string) { + this.values[this.union] = this.form.control.controls['value'].value + this.form.control.setControl( + 'value', + this.formService.getFormGroup( + union ? this.spec.variants[union].spec : {}, + [], + this.values[union], + ), + { + emitEvent: false, + }, + ) + } + + ngOnChanges() { + this.selectSpec = this.formService.getUnionSelectSpec(this.spec, this.union) + if (this.union) this.onUnion(this.union) + } +} diff --git a/web/projects/ui/src/app/components/form/form.module.ts b/web/projects/ui/src/app/components/form/form.module.ts new file mode 100644 index 000000000..f57f6ef46 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form.module.ts @@ -0,0 +1,109 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MaskitoModule } from '@maskito/angular' +import { TuiMapperPipeModule, TuiValueChangesModule } from '@taiga-ui/cdk' +import { + TuiErrorModule, + TuiExpandModule, + TuiHintModule, + TuiLinkModule, + TuiModeModule, + TuiTextfieldControllerModule, + TuiTooltipModule, +} from '@taiga-ui/core' +import { + TuiAppearanceModule, + TuiButtonModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { + TuiElasticContainerModule, + TuiFieldErrorPipeModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputTimeModule, + TuiMultiSelectModule, + TuiPromptModule, + TuiSelectModule, + TuiTagModule, + TuiTextareaModule, + TuiToggleModule, +} from '@taiga-ui/kit' + +import { FormGroupComponent } from './form-group/form-group.component' +import { FormTextComponent } from './form-text/form-text.component' +import { FormToggleComponent } from './form-toggle/form-toggle.component' +import { FormTextareaComponent } from './form-textarea/form-textarea.component' +import { FormNumberComponent } from './form-number/form-number.component' +import { FormSelectComponent } from './form-select/form-select.component' +import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component' +import { FormUnionComponent } from './form-union/form-union.component' +import { FormObjectComponent } from './form-object/form-object.component' +import { FormArrayComponent } from './form-array/form-array.component' +import { FormControlComponent } from './form-control/form-control.component' +import { MustachePipe } from './mustache.pipe' +import { ControlDirective } from './control.directive' +import { FormColorComponent } from './form-color/form-color.component' +import { FormDatetimeComponent } from './form-datetime/form-datetime.component' +import { HintPipe } from './hint.pipe' +import { FilterHiddenPipe } from './filter-hidden.pipe' + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputFilesModule, + TuiTextareaModule, + TuiSelectModule, + TuiMultiSelectModule, + TuiToggleModule, + TuiTooltipModule, + TuiHintModule, + TuiModeModule, + TuiTagModule, + TuiButtonModule, + TuiExpandModule, + TuiTextfieldControllerModule, + TuiLinkModule, + TuiPromptModule, + TuiErrorModule, + TuiFieldErrorPipeModule, + TuiValueChangesModule, + TuiElasticContainerModule, + MaskitoModule, + TuiIconModule, + TuiAppearanceModule, + TuiInputDateModule, + TuiInputTimeModule, + TuiInputDateTimeModule, + TuiMapperPipeModule, + ], + declarations: [ + FormGroupComponent, + FormControlComponent, + FormColorComponent, + FormDatetimeComponent, + FormTextComponent, + FormToggleComponent, + FormTextareaComponent, + FormNumberComponent, + FormSelectComponent, + FormMultiselectComponent, + FormUnionComponent, + FormObjectComponent, + FormArrayComponent, + MustachePipe, + HintPipe, + ControlDirective, + FilterHiddenPipe, + ], + exports: [FormGroupComponent], +}) +export class FormModule {} diff --git a/web/projects/ui/src/app/components/form/hint.pipe.ts b/web/projects/ui/src/app/components/form/hint.pipe.ts new file mode 100644 index 000000000..f03d9577f --- /dev/null +++ b/web/projects/ui/src/app/components/form/hint.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { IST } from '@start9labs/start-sdk' + +@Pipe({ + name: 'hint', +}) +export class HintPipe implements PipeTransform { + transform(spec: Exclude): string { + const hint = [] + + if (spec.description) { + hint.push(spec.description) + } + + if ('disabled' in spec && typeof spec.disabled === 'string') { + hint.push(`Disabled: ${spec.disabled}`) + } + + return hint.join('\n\n') + } +} diff --git a/web/projects/ui/src/app/components/form/invalid.service.ts b/web/projects/ui/src/app/components/form/invalid.service.ts new file mode 100644 index 000000000..9f474e853 --- /dev/null +++ b/web/projects/ui/src/app/components/form/invalid.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core' +import { ControlDirective } from './control.directive' + +@Injectable() +export class InvalidService { + private readonly controls: ControlDirective[] = [] + + scrollIntoView() { + this.controls.find(({ invalid }) => invalid)?.scrollIntoView() + } + + add(control: ControlDirective) { + this.controls.push(control) + } + + remove(control: ControlDirective) { + this.controls.splice(this.controls.indexOf(control), 1) + } +} diff --git a/web/projects/ui/src/app/components/form/mustache.pipe.ts b/web/projects/ui/src/app/components/form/mustache.pipe.ts new file mode 100644 index 000000000..ec04b0104 --- /dev/null +++ b/web/projects/ui/src/app/components/form/mustache.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core' + +const Mustache = require('mustache') + +@Pipe({ + name: 'mustache', +}) +export class MustachePipe implements PipeTransform { + transform(value: any, displayAs: string): string { + return displayAs && Mustache.render(displayAs, value) + } +} diff --git a/web/projects/ui/src/app/components/interface-info/interface-info.component.html b/web/projects/ui/src/app/components/interface-info/interface-info.component.html new file mode 100644 index 000000000..66d773063 --- /dev/null +++ b/web/projects/ui/src/app/components/interface-info/interface-info.component.html @@ -0,0 +1,62 @@ + + + +

{{ iFace.name }}

+

{{ iFace.description }}

+ + Add Domain + + + Make {{ iFace.public ? 'Private' : 'Public' }} + +
+
+
+ + +

{{ address.name }}

+

{{ address.url }}

+ + Remove + + + Remove + +
+ + + + + + + + + + + + + + +
+
diff --git a/web/projects/ui/src/app/components/interface-info/interface-info.component.scss b/web/projects/ui/src/app/components/interface-info/interface-info.component.scss new file mode 100644 index 000000000..61ead3b94 --- /dev/null +++ b/web/projects/ui/src/app/components/interface-info/interface-info.component.scss @@ -0,0 +1,3 @@ +p { + font-family: 'Courier New'; +} \ No newline at end of file diff --git a/web/projects/ui/src/app/components/interface-info/interface-info.component.ts b/web/projects/ui/src/app/components/interface-info/interface-info.component.ts new file mode 100644 index 000000000..932b1f767 --- /dev/null +++ b/web/projects/ui/src/app/components/interface-info/interface-info.component.ts @@ -0,0 +1,393 @@ +import { Component, Inject, Input } from '@angular/core' +import { WINDOW } from '@ng-web-apis/common' +import { + AlertController, + ModalController, + ToastController, +} from '@ionic/angular' +import { + copyToClipboard, + ErrorService, + LoadingService, +} from '@start9labs/shared' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { PatchDB } from 'patch-db-client' +import { QRComponent } from 'src/app/components/qr/qr.component' +import { firstValueFrom } from 'rxjs' +import { ISB, T, utils } from '@start9labs/start-sdk' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { FormComponent } from 'src/app/components/form.component' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { toAcmeName } from 'src/app/util/acme' +import { ConfigService } from 'src/app/services/config.service' + +export type MappedInterface = T.ServiceInterface & { + addresses: MappedAddress[] + public: boolean +} +export type MappedAddress = { + name: string + url: string + isDomain: boolean + isOnion: boolean + acme: string | null +} + +@Component({ + selector: 'interface-info', + templateUrl: './interface-info.component.html', + styleUrls: ['./interface-info.component.scss'], +}) +export class InterfaceInfoComponent { + @Input() pkgId?: string + @Input() iFace!: MappedInterface + + constructor( + private readonly toastCtrl: ToastController, + private readonly modalCtrl: ModalController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, + private readonly api: ApiService, + private readonly formDialog: FormDialogService, + private readonly alertCtrl: AlertController, + private readonly patch: PatchDB, + private readonly config: ConfigService, + @Inject(WINDOW) private readonly windowRef: Window, + ) {} + + launch(url: string): void { + this.windowRef.open(url, '_blank', 'noreferrer') + } + + async togglePublic() { + const loader = this.loader + .open(`Making ${this.iFace.public ? 'private' : 'public'}`) + .subscribe() + + const params = { + internalPort: this.iFace.addressInfo.internalPort, + public: !this.iFace.public, + } + + try { + if (this.pkgId) { + await this.api.pkgBindingSetPubic({ + ...params, + host: this.iFace.addressInfo.hostId, + package: this.pkgId, + }) + } else { + await this.api.serverBindingSetPubic(params) + } + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + async presentDomainForm() { + const acme = await firstValueFrom(this.patch.watch$('serverInfo', 'acme')) + + const spec = getDomainSpec(Object.keys(acme)) + + this.formDialog.open(FormComponent, { + label: 'Add Domain', + data: { + spec: await configBuilderToSpec(spec), + buttons: [ + { + text: 'Save', + handler: async (val: typeof spec._TYPE) => { + if (val.type.selection === 'standard') { + return this.saveStandard( + val.type.value.domain, + val.type.value.acme, + ) + } else { + return this.saveTor(val.type.value.key) + } + }, + }, + ], + }, + }) + } + + async removeStandard(url: string) { + const loader = this.loader.open('Removing').subscribe() + + const params = { + domain: new URL(url).hostname, + } + + try { + if (this.pkgId) { + await this.api.pkgRemoveDomain({ + ...params, + package: this.pkgId, + host: this.iFace.addressInfo.hostId, + }) + } else { + await this.api.serverRemoveDomain(params) + } + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + async removeOnion(url: string) { + const loader = this.loader.open('Removing').subscribe() + + const params = { + onion: new URL(url).hostname, + } + + try { + if (this.pkgId) { + await this.api.pkgRemoveOnion({ + ...params, + package: this.pkgId, + host: this.iFace.addressInfo.hostId, + }) + } else { + await this.api.serverRemoveOnion(params) + } + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + async showAcme(url: string | null): Promise { + const alert = await this.alertCtrl.create({ + header: 'ACME Provider', + message: toAcmeName(url), + }) + await alert.present() + } + + async showQR(text: string): Promise { + const modal = await this.modalCtrl.create({ + component: QRComponent, + componentProps: { + text, + }, + cssClass: 'qr-modal', + }) + await modal.present() + } + + async copy(address: string): Promise { + let message = '' + await copyToClipboard(address || '').then(success => { + message = success + ? 'Copied to clipboard!' + : 'Failed to copy to clipboard.' + }) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + }) + await toast.present() + } + + private async saveStandard(domain: string, acme: string) { + const loader = this.loader.open('Saving').subscribe() + + const params = { + domain, + acme: acme === 'none' ? null : acme, + private: false, + } + + try { + if (this.pkgId) { + await this.api.pkgAddDomain({ + ...params, + package: this.pkgId, + host: this.iFace.addressInfo.hostId, + }) + } else { + await this.api.serverAddDomain(params) + } + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + private async saveTor(key: string | null) { + const loader = this.loader.open('Creating onion address').subscribe() + + try { + let onion = key + ? await this.api.addTorKey({ key }) + : await this.api.generateTorKey({}) + onion = `${onion}.onion` + + if (this.pkgId) { + await this.api.pkgAddOnion({ + onion, + package: this.pkgId, + host: this.iFace.addressInfo.hostId, + }) + } else { + await this.api.serverAddOnion({ onion }) + } + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } +} + +function getDomainSpec(acme: string[]) { + return ISB.InputSpec.of({ + type: ISB.Value.union( + { name: 'Type', default: 'standard' }, + ISB.Variants.of({ + standard: { + name: 'Standard', + spec: ISB.InputSpec.of({ + domain: ISB.Value.text({ + name: 'Domain', + description: 'The domain or subdomain you want to use', + placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`, + required: true, + default: null, + patterns: [utils.Patterns.domain], + }), + acme: ISB.Value.select({ + name: 'ACME Provider', + description: + 'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.', + values: acme.reduce( + (obj, url) => ({ + ...obj, + [url]: toAcmeName(url), + }), + { none: 'None (use system Root CA)' } as Record, + ), + default: '', + }), + }), + }, + onion: { + name: 'Onion', + spec: ISB.InputSpec.of({ + key: ISB.Value.text({ + name: 'Private Key (optional)', + description: + 'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.', + required: false, + default: null, + patterns: [utils.Patterns.base64], + }), + }), + }, + }), + ), + }) +} + +export function getAddresses( + serviceInterface: T.ServiceInterface, + host: T.Host, + config: ConfigService, +): MappedAddress[] { + const addressInfo = serviceInterface.addressInfo + + let hostnames = host.hostnameInfo[addressInfo.internalPort] + + hostnames = hostnames.filter( + h => + config.isLocalhost() || + h.kind !== 'ip' || + h.hostname.kind !== 'ipv6' || + !h.hostname.value.startsWith('fe80::'), + ) + if (config.isLocalhost()) { + const local = hostnames.find( + h => h.kind === 'ip' && h.hostname.kind === 'local', + ) + if (local) { + hostnames.unshift({ + kind: 'ip', + networkInterfaceId: 'lo', + public: false, + hostname: { + kind: 'local', + port: local.hostname.port, + sslPort: local.hostname.sslPort, + value: 'localhost', + }, + }) + } + } + const mappedAddresses = hostnames.flatMap(h => { + let name = '' + let isDomain = false + let isOnion = false + let acme: string | null = null + + if (h.kind === 'onion') { + name = `Tor` + isOnion = true + } else { + const hostnameKind = h.hostname.kind + + if (hostnameKind === 'domain') { + name = 'Domain' + isDomain = true + acme = host.domains[h.hostname.domain]?.acme + } else { + name = + hostnameKind === 'local' + ? 'Local' + : `${h.networkInterfaceId} (${hostnameKind})` + } + } + + const addresses = utils.addressHostToUrl(addressInfo, h) + if (addresses.length > 1) { + return addresses.map(url => ({ + name: `${name} (${new URL(url).protocol + .replace(':', '') + .toUpperCase()})`, + url, + isDomain, + isOnion, + acme, + })) + } else { + return addresses.map(url => ({ + name, + url, + isDomain, + isOnion, + acme, + })) + } + }) + + return mappedAddresses.filter( + (value, index, self) => index === self.findIndex(t => t.url === value.url), + ) +} diff --git a/web/projects/ui/src/app/components/interface-info/interface-info.module.ts b/web/projects/ui/src/app/components/interface-info/interface-info.module.ts new file mode 100644 index 000000000..c31a6ae07 --- /dev/null +++ b/web/projects/ui/src/app/components/interface-info/interface-info.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { InterfaceInfoComponent } from './interface-info.component' + +@NgModule({ + declarations: [InterfaceInfoComponent], + imports: [CommonModule, IonicModule], + exports: [InterfaceInfoComponent], +}) +export class InterfaceInfoModule {} diff --git a/web/projects/ui/src/app/components/logs/logs.component.ts b/web/projects/ui/src/app/components/logs/logs.component.ts index 5f0688757..59d78dac7 100644 --- a/web/projects/ui/src/app/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/components/logs/logs.component.ts @@ -1,5 +1,15 @@ import { Component, Input, ViewChild } from '@angular/core' -import { IonContent, LoadingController } from '@ionic/angular' +import { IonContent } from '@ionic/angular' +import { + DownloadHTMLService, + ErrorService, + LoadingService, + Log, + LogsRes, + ServerLogsReq, + toLocalIsoString, +} from '@start9labs/shared' +import { TuiDestroyService } from '@taiga-ui/cdk' import { bufferTime, catchError, @@ -11,16 +21,6 @@ import { takeUntil, tap, } from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' -import { - LogsRes, - ServerLogsReq, - ErrorToastService, - toLocalIsoString, - Log, - DownloadHTMLService, -} from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConnectionService } from 'src/app/services/connection.service' @@ -39,7 +39,7 @@ var convert = new Convert({ selector: 'logs', templateUrl: './logs.component.html', styleUrls: ['./logs.component.scss'], - providers: [TuiDestroyService, DownloadHTMLService], + providers: [TuiDestroyService], }) export class LogsComponent { @ViewChild(IonContent) @@ -63,22 +63,22 @@ export class LogsComponent { | 'connected' | 'reconnecting' | 'disconnected' = 'connecting' - limit = 400 + limit = 200 count = 0 constructor( - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly destroy$: TuiDestroyService, private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly downloadHtml: DownloadHTMLService, - private readonly connectionService: ConnectionService, + private readonly connection$: ConnectionService, ) {} async ngOnInit() { from(this.followLogs({ limit: this.limit })) .pipe( - switchMap(({ 'start-cursor': startCursor, guid }) => { + switchMap(({ startCursor, guid }) => { this.startCursor = startCursor return this.connect$(guid) }), @@ -98,7 +98,7 @@ export class LogsComponent { this.processRes(res) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { e.target.complete() } @@ -120,10 +120,7 @@ export class LogsComponent { } async download() { - const loader = await this.loadingCtrl.create({ - message: 'Processing 10,000 logs...', - }) - await loader.present() + const loader = this.loader.open('Processing 10,000 logs...').subscribe() try { const { entries } = await this.fetchLogs({ @@ -140,52 +137,51 @@ export class LogsComponent { this.downloadHtml.download(`${this.context}-logs.html`, html, styles) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private reconnect$(): Observable { return from(this.followLogs({})).pipe( tap(_ => this.recordConnectionChange()), - switchMap(({ guid }) => this.connect$(guid, true)), + switchMap(({ guid }) => this.connect$(guid)), ) } - private connect$(guid: string, reconnect = false) { - const config: WebSocketSubjectConfig = { - url: `/rpc/${guid}`, - openObserver: { - next: () => { - this.websocketStatus = 'connected' + private connect$(guid: string) { + return this.api + .openWebsocket$(guid, { + openObserver: { + next: () => { + this.websocketStatus = 'connected' + }, }, - }, - } - - return this.api.openLogsWebsocket$(config).pipe( - tap(_ => this.count++), - bufferTime(1000), - tap(msgs => { - this.loading = false - this.processRes({ entries: msgs }) - if (this.infiniteStatus === 0 && this.count >= this.limit) - this.infiniteStatus = 1 - }), - catchError(() => { - this.recordConnectionChange(false) - return this.connectionService.connected$.pipe( - tap( - connected => - (this.websocketStatus = connected - ? 'reconnecting' - : 'disconnected'), - ), - filter(Boolean), - switchMap(() => this.reconnect$()), - ) - }), - ) + }) + .pipe( + tap(_ => this.count++), + bufferTime(1000), + tap(msgs => { + this.loading = false + this.processRes({ entries: msgs }) + if (this.infiniteStatus === 0 && this.count >= this.limit) + this.infiniteStatus = 1 + }), + catchError(() => { + this.recordConnectionChange(false) + return this.connection$.pipe( + tap( + connected => + (this.websocketStatus = connected + ? 'reconnecting' + : 'disconnected'), + ), + filter(Boolean), + switchMap(() => this.reconnect$()), + ) + }), + ) } private recordConnectionChange(success = true) { @@ -206,7 +202,7 @@ export class LogsComponent { } private processRes(res: LogsRes) { - const { entries, 'start-cursor': startCursor } = res + const { entries, startCursor } = res if (!entries.length) return diff --git a/web/projects/ui/src/app/components/status/status.component.html b/web/projects/ui/src/app/components/status/status.component.html index fd265fd96..65c142f4a 100644 --- a/web/projects/ui/src/app/components/status/status.component.html +++ b/web/projects/ui/src/app/components/status/status.component.html @@ -1,28 +1,20 @@

- {{ (connected$ | async) ? rendering.display : 'Unknown' }} + {{ (connection$ | async) ? rendering.display : 'Unknown' }} - - this may take a while + + . This may take a while - - - {{ progress }} + + + {{ installingInfo.progress.overall | installingProgressString }} diff --git a/web/projects/ui/src/app/components/status/status.component.module.ts b/web/projects/ui/src/app/components/status/status.component.module.ts index ea093b8e9..04fa4690c 100644 --- a/web/projects/ui/src/app/components/status/status.component.module.ts +++ b/web/projects/ui/src/app/components/status/status.component.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { UnitConversionPipesModule } from '@start9labs/shared' import { StatusComponent } from './status.component' -import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' @NgModule({ declarations: [StatusComponent], @@ -11,7 +11,7 @@ import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/instal CommonModule, IonicModule, UnitConversionPipesModule, - InstallProgressPipeModule, + InstallingProgressPipeModule, ], exports: [StatusComponent], }) diff --git a/web/projects/ui/src/app/components/status/status.component.ts b/web/projects/ui/src/app/components/status/status.component.ts index 14ece402c..45f66f291 100644 --- a/web/projects/ui/src/app/components/status/status.component.ts +++ b/web/projects/ui/src/app/components/status/status.component.ts @@ -1,9 +1,8 @@ import { Component, Input } from '@angular/core' import { ConnectionService } from 'src/app/services/connection.service' -import { InstallProgress } from 'src/app/services/patch-db/data-model' +import { InstallingInfo } from 'src/app/services/patch-db/data-model' import { PrimaryRendering, - PrimaryStatus, StatusRendering, } from 'src/app/services/pkg-status-rendering.service' @@ -13,17 +12,14 @@ import { styleUrls: ['./status.component.scss'], }) export class StatusComponent { - PS = PrimaryStatus PR = PrimaryRendering @Input() rendering!: StatusRendering @Input() size?: string @Input() style?: string = 'regular' @Input() weight?: string = 'normal' - @Input() installProgress?: InstallProgress + @Input() installingInfo?: InstallingInfo @Input() sigtermTimeout?: string | null = null - readonly connected$ = this.connectionService.connected$ - - constructor(private readonly connectionService: ConnectionService) {} + constructor(readonly connection$: ConnectionService) {} } diff --git a/web/projects/ui/src/app/components/toast-container/notifications-toast/notifications-toast.service.ts b/web/projects/ui/src/app/components/toast-container/notifications-toast/notifications-toast.service.ts index 52d35ea3c..a2368438d 100644 --- a/web/projects/ui/src/app/components/toast-container/notifications-toast/notifications-toast.service.ts +++ b/web/projects/ui/src/app/components/toast-container/notifications-toast/notifications-toast.service.ts @@ -7,7 +7,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @Injectable({ providedIn: 'root' }) export class NotificationsToastService extends Observable { private readonly stream$ = this.patch - .watch$('server-info', 'unread-notification-count') + .watch$('serverInfo', 'unreadNotificationCount') .pipe( pairwise(), map(([prev, cur]) => cur > prev), diff --git a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts index f039841d3..04943eb1a 100644 --- a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts +++ b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { SwUpdate } from '@angular/service-worker' +import { LoadingService } from '@start9labs/shared' import { merge, Observable, Subject } from 'rxjs' import { RefreshAlertService } from './refresh-alert.service' -import { SwUpdate } from '@angular/service-worker' -import { LoadingController } from '@ionic/angular' @Component({ selector: 'refresh-alert', @@ -18,7 +18,7 @@ export class RefreshAlertComponent { constructor( @Inject(RefreshAlertService) private readonly refresh$: Observable, private readonly updates: SwUpdate, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, ) {} ngOnInit() { @@ -26,17 +26,14 @@ export class RefreshAlertComponent { } async pwaReload() { - const loader = await this.loadingCtrl.create({ - message: 'Reloading PWA...', - }) - await loader.present() + const loader = this.loader.open('Reloading PWA...').subscribe() try { // attempt to update to the latest client version available await this.updates.activateUpdate() } catch (e) { console.error('Error activating update from service worker: ', e) } finally { - loader.dismiss() + loader.unsubscribe() // always reload, as this resolves most out of sync cases window.location.reload() } diff --git a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts index 43c7f82c4..d567e5956 100644 --- a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts +++ b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts @@ -1,21 +1,24 @@ import { Injectable } from '@angular/core' import { endWith, Observable } from 'rxjs' import { map } from 'rxjs/operators' -import { Emver } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { ConfigService } from '../../../services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root' }) export class RefreshAlertService extends Observable { - private readonly stream$ = this.patch.watch$('server-info', 'version').pipe( - map(version => !!this.emver.compare(this.config.version, version)), + private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe( + map( + version => + Version.parse(this.config.version).compare(Version.parse(version)) !== + 'equal', + ), endWith(false), ) constructor( private readonly patch: PatchDB, - private readonly emver: Emver, private readonly config: ConfigService, ) { super(subscriber => this.stream$.subscribe(subscriber)) diff --git a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts index 72b6956ee..276260ba8 100644 --- a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts +++ b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts @@ -1,10 +1,9 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' -import { LoadingController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' -import { Observable, Subject, merge } from 'rxjs' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { merge, Observable, Subject } from 'rxjs' +import { ApiService } from '../../../services/api/embassy-api.service' import { UpdateToastService } from './update-toast.service' -import { ApiService } from '../../../services/api/embassy-api.service' @Component({ selector: 'update-toast', @@ -19,8 +18,8 @@ export class UpdateToastComponent { constructor( @Inject(UpdateToastService) private readonly update$: Observable, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, ) {} onDismiss() { @@ -30,18 +29,14 @@ export class UpdateToastComponent { async restart(): Promise { this.onDismiss() - const loader = await this.loadingCtrl.create({ - message: 'Restarting...', - }) - - await loader.present() + const loader = this.loader.open('Restarting...').subscribe() try { await this.embassyApi.restartServer({}) } catch (e: any) { - await this.errToast.present(e) + await this.errorService.handleError(e) } finally { - await loader.dismiss() + await loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.service.ts b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.service.ts index 5eb650869..819ed3dc6 100644 --- a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.service.ts +++ b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.service.ts @@ -7,7 +7,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @Injectable({ providedIn: 'root' }) export class UpdateToastService extends Observable { private readonly stream$ = this.patch - .watch$('server-info', 'status-info', 'updated') + .watch$('serverInfo', 'statusInfo', 'updated') .pipe(distinctUntilChanged(), filter(Boolean), endWith(false)) constructor(private readonly patch: PatchDB) { diff --git a/web/projects/ui/src/app/modals/action-input.component.ts b/web/projects/ui/src/app/modals/action-input.component.ts new file mode 100644 index 000000000..4543e29fd --- /dev/null +++ b/web/projects/ui/src/app/modals/action-input.component.ts @@ -0,0 +1,217 @@ +import { CommonModule } from '@angular/common' +import { Component, Inject } from '@angular/core' +import { getErrorMessage } from '@start9labs/shared' +import { T, utils } from '@start9labs/start-sdk' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { + TuiDialogContext, + TuiDialogService, + TuiLoaderModule, + TuiModeModule, + TuiNotificationModule, +} from '@taiga-ui/core' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { compare } from 'fast-json-patch' +import { PatchDB } from 'patch-db-client' +import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs' +import { InvalidService } from 'src/app/components/form/invalid.service' +import { ActionRequestInfoComponent } from 'src/app/modals/action-request-input.component' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { getAllPackages, getManifest } from 'src/app/util/get-package-data' +import * as json from 'fast-json-patch' +import { ActionService } from '../services/action.service' +import { ActionButton, FormComponent } from '../components/form.component' + +export type PackageActionData = { + pkgInfo: { + id: string + title: string + icon: string + mainStatus: T.MainStatus['main'] + } + actionInfo: { + id: string + metadata: T.ActionMetadata + } + requestInfo?: { + dependentId?: string + request: T.ActionRequest + } +} + +@Component({ + template: ` +

+ +

{{ pkgInfo.title }}

+
+ + +
+
+ + + +
+
+ + + + + + +
+
+ + + + + `, + styles: [ + ` + tui-notification { + font-size: 1rem; + margin-bottom: 1.4rem; + } + .service-title { + display: inline-flex; + align-items: center; + margin-bottom: 1.4rem; + img { + height: 20px; + margin-right: 4px; + } + h4 { + margin: 0; + } + } + `, + ], + standalone: true, + imports: [ + CommonModule, + TuiLoaderModule, + TuiNotificationModule, + TuiButtonModule, + TuiModeModule, + ActionRequestInfoComponent, + UiPipeModule, + FormComponent, + ], + providers: [InvalidService], +}) +export class ActionInputModal { + readonly actionId = this.context.data.actionInfo.id + readonly warning = this.context.data.actionInfo.metadata.warning + readonly pkgInfo = this.context.data.pkgInfo + readonly requestInfo = this.context.data.requestInfo + + buttons: ActionButton[] = [ + { + text: 'Submit', + handler: value => this.execute(value), + }, + ] + + error = '' + + res$ = defer(() => + this.api.getActionInput({ + packageId: this.pkgInfo.id, + actionId: this.actionId, + }), + ).pipe( + map(res => { + const originalValue = res.value || {} + + return { + spec: res.spec, + originalValue, + operations: this.requestInfo?.request.input + ? compare( + JSON.parse(JSON.stringify(originalValue)), + utils.deepMerge( + JSON.parse(JSON.stringify(originalValue)), + this.requestInfo.request.input.value, + ) as object, + ) + : null, + } + }), + catchError(e => { + this.error = String(getErrorMessage(e)) + return EMPTY + }), + ) + + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + private readonly dialogs: TuiDialogService, + private readonly api: ApiService, + private readonly patch: PatchDB, + private readonly actionService: ActionService, + ) {} + + async execute(input: object) { + if (await this.checkConflicts(input)) { + return this.actionService.execute(this.pkgInfo.id, this.actionId, input) + } + } + + private async checkConflicts(input: object): Promise { + const packages = await getAllPackages(this.patch) + + const breakages = Object.keys(packages) + .filter( + id => + id !== this.pkgInfo.id && + Object.values(packages[id].requestedActions).some( + ({ request, active }) => + !active && + request.severity === 'critical' && + request.packageId === this.pkgInfo.id && + request.actionId === this.actionId && + request.when?.condition === 'input-not-matches' && + request.input && + json + .compare(input, request.input) + .some(op => op.op === 'add' || op.op === 'replace'), + ), + ) + .map(id => id) + + if (!breakages.length) return true + + const message = + 'As a result of this change, the following services will no longer work properly and may crash:
    ' + const content = `${message}${breakages.map( + id => `
  • ${getManifest(packages[id]).title}
  • `, + )}
` + const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' } + + return firstValueFrom( + this.dialogs.open(TUI_PROMPT, { data }).pipe(endWith(false)), + ) + } +} diff --git a/web/projects/ui/src/app/modals/action-request-input.component.ts b/web/projects/ui/src/app/modals/action-request-input.component.ts new file mode 100644 index 000000000..2451de876 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-request-input.component.ts @@ -0,0 +1,100 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, +} from '@angular/core' +import { getValueByPointer, Operation } from 'fast-json-patch' +import { isObject } from '@start9labs/shared' +import { tuiIsNumber } from '@taiga-ui/cdk' +import { CommonModule } from '@angular/common' +import { TuiNotificationModule } from '@taiga-ui/core' + +@Component({ + selector: 'action-request-info', + template: ` + + The following modifications were made: +
    +
  • +
+
+ `, + standalone: true, + imports: [CommonModule, TuiNotificationModule], + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + tui-notification { + margin-bottom: 1.5rem; + } + `, + ], +}) +export class ActionRequestInfoComponent implements OnInit { + @Input() + originalValue: object = {} + + @Input() + operations: Operation[] = [] + + diff: string[] = [] + + ngOnInit() { + this.diff = this.operations.map( + op => `${this.getPath(op)}: ${this.getMessage(op)}`, + ) + } + + private getPath(operation: Operation): string { + const path = operation.path + .substring(1) + .split('/') + .map(node => { + const num = Number(node) + return isNaN(num) ? node : num + }) + + if (tuiIsNumber(path[path.length - 1])) { + path.pop() + } + + return path.join(' → ') + } + + private getMessage(operation: Operation): string { + switch (operation.op) { + case 'add': + return `added ${this.getNewValue(operation.value)}` + case 'remove': + return `removed ${this.getOldValue(operation.path)}` + case 'replace': + return `changed from ${this.getOldValue( + operation.path, + )} to ${this.getNewValue(operation.value)}` + default: + return `Unknown operation` // unreachable + } + } + + private getOldValue(path: any): string { + const val = getValueByPointer(this.originalValue, path) + if (['string', 'number', 'boolean'].includes(typeof val)) { + return val + } else if (isObject(val)) { + return 'entry' + } else { + return 'list' + } + } + + private getNewValue(val: any): string { + if (['string', 'number', 'boolean'].includes(typeof val)) { + return val + } else if (isObject(val)) { + return 'new entry' + } else { + return 'new list' + } + } +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts b/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts new file mode 100644 index 000000000..480146668 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts @@ -0,0 +1,48 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { TuiFadeModule, TuiTitleModule } from '@taiga-ui/experimental' +import { TuiAccordionModule } from '@taiga-ui/kit' +import { ActionSuccessMemberComponent } from './action-success-member.component' +import { GroupResult } from './types' + +@Component({ + standalone: true, + selector: 'app-action-success-group', + template: ` +

+ + +

{{ member.name }}
+ + + + +

+ `, + styles: [ + ` + p:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + TuiTitleModule, + ActionSuccessMemberComponent, + TuiAccordionModule, + TuiFadeModule, + ], +}) +export class ActionSuccessGroupComponent { + @Input() + group!: GroupResult +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success-member.component.ts b/web/projects/ui/src/app/modals/action-success/action-success-member.component.ts new file mode 100644 index 000000000..55bffc7bc --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/action-success-member.component.ts @@ -0,0 +1,169 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + ElementRef, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { + TuiDialogService, + TuiTextfieldComponent, + TuiTextfieldControllerModule, +} from '@taiga-ui/core' +import { TuiButtonModule, TuiTitleModule } from '@taiga-ui/experimental' +import { TuiInputModule } from '@taiga-ui/kit' +import { QrCodeModule } from 'ng-qrcode' +import { T } from '@start9labs/start-sdk' + +@Component({ + standalone: true, + selector: 'app-action-success-member', + template: ` + + {{ member.name }} + + + + + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + .reveal { + @include center-all(); + } + + .qr { + position: relative; + text-align: center; + } + `, + ], + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiTextfieldControllerModule, + TuiButtonModule, + QrCodeModule, + TuiTitleModule, + ], +}) +export class ActionSuccessMemberComponent { + @ViewChild(TuiTextfieldComponent, { read: ElementRef }) + private readonly input!: ElementRef + private readonly dialogs = inject(TuiDialogService) + + @Input() + member!: T.ActionResultMember & { type: 'single' } + + masked = true + + get border(): number { + let border = 0 + + if (this.member.masked) border += 2 + if (this.member.copyable) border += 2 + if (this.member.qr) border += 2 + + return border + } + + show(template: TemplateRef) { + const masked = this.masked + + this.masked = this.member.masked + this.dialogs + .open(template, { label: 'Scan this QR', size: 's' }) + .subscribe({ + complete: () => (this.masked = masked), + }) + } + + copy() { + const el = this.input.nativeElement + + if (!el) { + return + } + + el.type = 'text' + el.focus() + el.select() + el.ownerDocument.execCommand('copy') + el.type = this.masked && this.member.masked ? 'password' : 'text' + } +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success-single.component.ts b/web/projects/ui/src/app/modals/action-success/action-success-single.component.ts new file mode 100644 index 000000000..2902c3c88 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/action-success-single.component.ts @@ -0,0 +1,145 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + ElementRef, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { + TuiDialogService, + TuiLabelModule, + TuiTextfieldComponent, + TuiTextfieldControllerModule, +} from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiInputModule } from '@taiga-ui/kit' +import { QrCodeModule } from 'ng-qrcode' +import { SingleResult } from './types' + +@Component({ + standalone: true, + selector: 'app-action-success-single', + template: ` +

+ +

+ + + + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + .reveal { + @include center-all(); + } + + .qr { + position: relative; + text-align: center; + } + `, + ], + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiTextfieldControllerModule, + TuiButtonModule, + QrCodeModule, + TuiLabelModule, + ], +}) +export class ActionSuccessSingleComponent { + @ViewChild(TuiTextfieldComponent, { read: ElementRef }) + private readonly input!: ElementRef + private readonly dialogs = inject(TuiDialogService) + + @Input() + single!: SingleResult + + masked = true + + get border(): number { + let border = 0 + + if (this.single.masked) border += 2 + if (this.single.copyable) border += 2 + + return border + } + + copy() { + const el = this.input.nativeElement + + if (!el) { + return + } + + el.type = 'text' + el.focus() + el.select() + el.ownerDocument.execCommand('copy') + el.type = this.masked && this.single.masked ? 'password' : 'text' + } +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success.module.ts b/web/projects/ui/src/app/modals/action-success/action-success.module.ts deleted file mode 100644 index 23c123081..000000000 --- a/web/projects/ui/src/app/modals/action-success/action-success.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { ActionSuccessPage } from './action-success.page' -import { QrCodeModule } from 'ng-qrcode' - -@NgModule({ - declarations: [ActionSuccessPage], - imports: [CommonModule, IonicModule, QrCodeModule], - exports: [ActionSuccessPage], -}) -export class ActionSuccessPageModule {} diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.html b/web/projects/ui/src/app/modals/action-success/action-success.page.html deleted file mode 100644 index da8cc7be5..000000000 --- a/web/projects/ui/src/app/modals/action-success/action-success.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - Execution Complete - - - - - - - - - -

{{ actionRes.message }}

- -
-
- -
- -

{{ actionRes.value }}

- - {{ actionRes.value }} - - - - -
-
diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.ts b/web/projects/ui/src/app/modals/action-success/action-success.page.ts index 30c5c02cd..7f390e2ab 100644 --- a/web/projects/ui/src/app/modals/action-success/action-success.page.ts +++ b/web/projects/ui/src/app/modals/action-success/action-success.page.ts @@ -1,39 +1,36 @@ -import { Component, Input } from '@angular/core' -import { ModalController, ToastController } from '@ionic/angular' -import { ActionResponse } from 'src/app/services/api/api.types' -import { copyToClipboard } from '@start9labs/shared' +import { CommonModule } from '@angular/common' +import { Component, inject } from '@angular/core' +import { TuiDialogContext } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { ActionSuccessGroupComponent } from './action-success-group.component' +import { ActionSuccessSingleComponent } from './action-success-single.component' +import { ActionResponseWithResult } from './types' @Component({ - selector: 'action-success', - templateUrl: './action-success.page.html', - styleUrls: ['./action-success.page.scss'], + standalone: true, + template: ` +

{{ data.message }}

+ + + `, + imports: [ + CommonModule, + ActionSuccessGroupComponent, + ActionSuccessSingleComponent, + ], }) export class ActionSuccessPage { - @Input() - actionRes!: ActionResponse + readonly data = + inject>( + POLYMORPHEUS_CONTEXT, + ).data - constructor( - private readonly modalCtrl: ModalController, - private readonly toastCtrl: ToastController, - ) {} - - async copy(address: string) { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async dismiss() { - return this.modalCtrl.dismiss() - } + readonly single = this.data.result.type === 'single' ? this.data.result : null + readonly group = this.data.result.type === 'group' ? this.data.result : null } diff --git a/web/projects/ui/src/app/modals/action-success/types.ts b/web/projects/ui/src/app/modals/action-success/types.ts new file mode 100644 index 000000000..efc515195 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/types.ts @@ -0,0 +1,7 @@ +import { RR } from 'src/app/services/api/api.types' + +type ActionResponse = NonNullable +type ActionResult = NonNullable +export type ActionResponseWithResult = ActionResponse & { result: ActionResult } +export type SingleResult = ActionResult & { type: 'single' } +export type GroupResult = ActionResult & { type: 'group' } diff --git a/web/projects/ui/src/app/modals/app-config/app-config.module.ts b/web/projects/ui/src/app/modals/app-config/app-config.module.ts deleted file mode 100644 index fde422826..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { IonicModule } from '@ionic/angular' -import { AppConfigPage } from './app-config.page' -import { TextSpinnerComponentModule } from '@start9labs/shared' -import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module' - -@NgModule({ - declarations: [AppConfigPage], - imports: [ - CommonModule, - FormsModule, - IonicModule, - TextSpinnerComponentModule, - FormObjectComponentModule, - ReactiveFormsModule, - ], - exports: [AppConfigPage], -}) -export class AppConfigPageModule {} diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.html b/web/projects/ui/src/app/modals/app-config/app-config.page.html deleted file mode 100644 index 49eec5f5f..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.html +++ /dev/null @@ -1,149 +0,0 @@ - - - Config - - - - - - - - - - - - - - - - - {{ loadingError }} - - - - - - -

- - {{ pkg.manifest.title }} has been automatically configured with - recommended defaults. Make whatever changes you want, then click - "Save". - -

-
- -

- - New config options! To accept the default values, click "Save". - You may also customize these new options below. - -

-
-
- - - - -

- - - {{ pkg.manifest.title }} - -

-

- - The following modifications have been made to {{ - pkg.manifest.title }} to satisfy {{ dependentInfo.title }}: -

    -
  • -
- To accept these modifications, click "Save". - -

-
-
- - - - -

- No config options for {{ pkg.manifest.title }} {{ - pkg.manifest.version }}. -

-
-
- - -
- -
-
-
-
- - - - - - - - Reset Defaults - - - - - Save - - - Close - - - - - diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.scss b/web/projects/ui/src/app/modals/app-config/app-config.page.scss deleted file mode 100644 index e568528a8..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.scss +++ /dev/null @@ -1,12 +0,0 @@ -.notifier-item { - margin: 12px; - margin-top: 0px; - border-radius: 12px; - // kills the lines - --border-width: 0; - --inner-border-width: 0; -} - -.header-details { - font-size: 20px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.ts b/web/projects/ui/src/app/modals/app-config/app-config.page.ts deleted file mode 100644 index 35388b36c..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { Component, Input } from '@angular/core' -import { - AlertController, - ModalController, - LoadingController, - IonicSafeString, -} from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - ErrorToastService, - getErrorMessage, - isEmptyObject, - isObject, -} from '@start9labs/shared' -import { DependentInfo } from 'src/app/types/dependent-info' -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { - DataModel, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' -import { PatchDB } from 'patch-db-client' -import { UntypedFormGroup } from '@angular/forms' -import { - convertValuesRecursive, - FormService, -} from 'src/app/services/form.service' -import { compare, Operation, getValueByPointer } from 'fast-json-patch' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { getAllPackages, getPackage } from 'src/app/util/get-package-data' -import { Breakages } from 'src/app/services/api/api.types' - -@Component({ - selector: 'app-config', - templateUrl: './app-config.page.html', - styleUrls: ['./app-config.page.scss'], -}) -export class AppConfigPage { - @Input() pkgId!: string - - @Input() dependentInfo?: DependentInfo - - pkg!: PackageDataEntry - loadingText = '' - - configSpec?: ConfigSpec - configForm?: UntypedFormGroup - - original?: object // only if existing config - diff?: string[] // only if dependent info - - loading = true - hasNewOptions = false - saving = false - loadingError: string | IonicSafeString = '' - - hasOptions = false - - constructor( - private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - private readonly patch: PatchDB, - ) {} - - async ngOnInit() { - try { - const pkg = await getPackage(this.patch, this.pkgId) - if (!pkg) return - this.pkg = pkg - - if (!this.pkg.manifest.config) return - - let newConfig: object | undefined - let patch: Operation[] | undefined - - if (this.dependentInfo) { - this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}` - const { - 'old-config': oc, - 'new-config': nc, - spec: s, - } = await this.embassyApi.dryConfigureDependency({ - 'dependency-id': this.pkgId, - 'dependent-id': this.dependentInfo.id, - }) - this.original = oc - newConfig = nc - this.configSpec = s - patch = compare(this.original, newConfig) - } else { - this.loadingText = 'Loading Config' - const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ - id: this.pkgId, - }) - this.original = c - this.configSpec = s - } - - this.configForm = this.formService.createForm( - this.configSpec, - newConfig || this.original, - ) - - this.hasOptions = !!Object.values(this.configSpec).find( - valSpec => valSpec.type !== 'pointer', - ) - - if (patch) { - this.diff = this.getDiff(patch) - this.markDirty(patch) - } - } catch (e: any) { - this.loadingError = getErrorMessage(e) - } finally { - this.loading = false - } - } - - resetDefaults() { - this.configForm = this.formService.createForm(this.configSpec!) - const patch = compare(this.original || {}, this.configForm.value) - this.markDirty(patch) - } - - async dismiss() { - if (this.configForm?.dirty) { - this.presentAlertUnsaved() - } else { - this.modalCtrl.dismiss() - } - } - - async tryConfigure() { - convertValuesRecursive(this.configSpec!, this.configForm!) - - if (this.configForm!.invalid) { - document - .getElementsByClassName('validation-error')[0] - ?.scrollIntoView({ behavior: 'smooth' }) - return - } - - this.saving = true - - if (hasCurrentDeps(this.pkg)) { - this.dryConfigure() - } else { - this.configure() - } - } - - private async dryConfigure() { - const loader = await this.loadingCtrl.create({ - message: 'Checking dependent services...', - }) - await loader.present() - - try { - const breakages = await this.embassyApi.drySetPackageConfig({ - id: this.pkgId, - config: this.configForm!.value, - }) - - if (isEmptyObject(breakages)) { - this.configure(loader) - } else { - await loader.dismiss() - const proceed = await this.presentAlertBreakages(breakages) - if (proceed) { - this.configure() - } else { - this.saving = false - } - } - } catch (e: any) { - this.errToast.present(e) - this.saving = false - loader.dismiss() - } - } - - private async configure(loader?: HTMLIonLoadingElement) { - const message = 'Saving...' - if (loader) { - loader.message = message - } else { - loader = await this.loadingCtrl.create({ message }) - await loader.present() - } - - try { - await this.embassyApi.setPackageConfig({ - id: this.pkgId, - config: this.configForm!.value, - }) - this.modalCtrl.dismiss() - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - loader.dismiss() - } - } - - private async presentAlertBreakages(breakages: Breakages): Promise { - let message: string = - 'As a result of this change, the following services will no longer work properly and may crash:
    ' - const localPkgs = await getAllPackages(this.patch) - const bullets = Object.keys(breakages).map(id => { - const title = localPkgs[id].manifest.title - return `
  • ${title}
  • ` - }) - message = `${message}${bullets}
` - - return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, - }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() - }) - } - - private getDiff(patch: Operation[]): string[] { - return patch.map(op => { - let message: string - switch (op.op) { - case 'add': - message = `Added ${this.getNewValue(op.value)}` - break - case 'remove': - message = `Removed ${this.getOldValue(op.path)}` - break - case 'replace': - message = `Changed from ${this.getOldValue( - op.path, - )} to ${this.getNewValue(op.value)}` - break - default: - message = `Unknown operation` - } - - let displayPath: string - - const arrPath = op.path - .substring(1) - .split('/') - .map(node => { - const num = Number(node) - return isNaN(num) ? node : num - }) - - if (typeof arrPath[arrPath.length - 1] === 'number') { - arrPath.pop() - } - - displayPath = arrPath.join(' → ') - - return `${displayPath}: ${message}` - }) - } - - private getOldValue(path: any): string { - const val = getValueByPointer(this.original, path) - if (['string', 'number', 'boolean'].includes(typeof val)) { - return val - } else if (isObject(val)) { - return 'entry' - } else { - return 'list' - } - } - - private getNewValue(val: any): string { - if (['string', 'number', 'boolean'].includes(typeof val)) { - return val - } else if (isObject(val)) { - return 'new entry' - } else { - return 'new list' - } - } - - private markDirty(patch: Operation[]) { - patch.forEach(op => { - const arrPath = op.path - .substring(1) - .split('/') - .map(node => { - const num = Number(node) - return isNaN(num) ? node : num - }) - - if (op.op !== 'remove') this.configForm!.get(arrPath)?.markAsDirty() - - if (typeof arrPath[arrPath.length - 1] === 'number') { - const prevPath = arrPath.slice(0, arrPath.length - 1) - this.configForm!.get(prevPath)?.markAsDirty() - } - }) - } - - private async presentAlertUnsaved() { - const alert = await this.alertCtrl.create({ - header: 'Unsaved Changes', - message: 'You have unsaved changes. Are you sure you want to leave?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: `Leave`, - handler: () => { - this.modalCtrl.dismiss() - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } -} diff --git a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html index 09a055650..f1372f468 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html +++ b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html @@ -1,5 +1,5 @@ @@ -18,8 +18,8 @@

{{ option.title }}

Version {{ option.version }}

-

Backup made: {{ option.timestamp | date : 'medium' }}

-

+

Created: {{ option.timestamp | date : 'medium' }}

+

Ready to restore

@@ -27,7 +27,7 @@

{{ option.title }}

Unavailable. {{ option.title }} is already installed.

-

+

Unavailable. Backup was made on a newer version of StartOS. @@ -36,7 +36,7 @@

{{ option.title }}

diff --git a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts index 65f61e643..c9869bce6 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts @@ -1,16 +1,12 @@ import { Component, Input } from '@angular/core' -import { - LoadingController, - ModalController, - IonicSafeString, -} from '@ionic/angular' -import { getErrorMessage } from '@start9labs/shared' +import { IonicSafeString, ModalController } from '@ionic/angular' +import { getErrorMessage, LoadingService } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { take } from 'rxjs' import { BackupInfo } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PatchDB } from 'patch-db-client' -import { AppRecoverOption } from './to-options.pipe' import { DataModel } from 'src/app/services/patch-db/data-model' -import { take } from 'rxjs' +import { AppRecoverOption } from './to-options.pipe' @Component({ selector: 'app-recover-select', @@ -18,19 +14,19 @@ import { take } from 'rxjs' styleUrls: ['./app-recover-select.page.scss'], }) export class AppRecoverSelectPage { - @Input() id!: string + @Input() targetId!: string + @Input() serverId!: string @Input() backupInfo!: BackupInfo @Input() password!: string - @Input() oldPassword?: string - readonly packageData$ = this.patch.watch$('package-data').pipe(take(1)) + readonly packageData$ = this.patch.watch$('packageData').pipe(take(1)) hasSelection = false error: string | IonicSafeString = '' constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly patch: PatchDB, ) {} @@ -45,23 +41,20 @@ export class AppRecoverSelectPage { async restore(options: AppRecoverOption[]): Promise { const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id) - const loader = await this.loadingCtrl.create({ - message: 'Initializing...', - }) - await loader.present() + const loader = this.loader.open('Initializing...').subscribe() try { await this.embassyApi.restorePackages({ ids, - 'target-id': this.id, - 'old-password': this.oldPassword || null, + targetId: this.targetId, + serverId: this.serverId, password: this.password, }) this.modalCtrl.dismiss(undefined, 'success') } catch (e: any) { this.error = getErrorMessage(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts index 2ac688a6b..206e97f71 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts @@ -1,16 +1,17 @@ import { Pipe, PipeTransform } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { PackageBackupInfo } from 'src/app/services/api/api.types' import { ConfigService } from 'src/app/services/config.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' +import { Version } from '@start9labs/start-sdk' export interface AppRecoverOption extends PackageBackupInfo { id: string checked: boolean installed: boolean - 'newer-eos': boolean + newerOS: boolean } @Pipe({ @@ -19,7 +20,7 @@ export interface AppRecoverOption extends PackageBackupInfo { export class ToOptionsPipe implements PipeTransform { constructor( private readonly config: ConfigService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} transform( @@ -34,7 +35,10 @@ export class ToOptionsPipe implements PipeTransform { id, installed: !!packageData[id], checked: false, - 'newer-eos': this.compare(packageBackups[id]['os-version']), + newerOS: + Version.parse(packageBackups[id].osVersion).compare( + Version.parse(this.config.version), + ) === 'greater', })) .sort((a, b) => b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1, @@ -42,9 +46,4 @@ export class ToOptionsPipe implements PipeTransform { ), ) } - - private compare(version: string): boolean { - // checks to see if backup was made on a newer version of eOS - return this.emver.compare(version, this.config.version) === 1 - } } diff --git a/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts b/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts index 032c0c840..32770af76 100644 --- a/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts +++ b/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts @@ -1,9 +1,10 @@ import { Component } from '@angular/core' import { ModalController } from '@ionic/angular' -import { map, take } from 'rxjs/operators' -import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' +import { map } from 'rxjs/operators' +import { DataModel } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { firstValueFrom } from 'rxjs' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'backup-select', @@ -12,7 +13,7 @@ import { firstValueFrom } from 'rxjs' }) export class BackupSelectPage { hasSelection = false - selectAll = false + selectAll = true pkgs: { id: string title: string @@ -28,17 +29,17 @@ export class BackupSelectPage { async ngOnInit() { this.pkgs = await firstValueFrom( - this.patch.watch$('package-data').pipe( + this.patch.watch$('packageData').pipe( map(pkgs => { return Object.values(pkgs) .map(pkg => { - const { id, title } = pkg.manifest + const { id, title } = getManifest(pkg) return { id, title, - icon: pkg['static-files'].icon, - disabled: pkg.state !== PackageState.Installed, - checked: pkg.state === PackageState.Installed, + icon: pkg.icon, + disabled: pkg.stateInfo.state !== 'installed', + checked: false, } }) .sort((a, b) => diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts new file mode 100644 index 000000000..958b98dff --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { FormsModule } from '@angular/forms' +import { BackupServerSelectModal } from './backup-server-select.page' +import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module' + +@NgModule({ + declarations: [BackupServerSelectModal], + imports: [CommonModule, FormsModule, IonicModule, AppRecoverSelectPageModule], + exports: [BackupServerSelectModal], +}) +export class BackupServerSelectModule {} diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html new file mode 100644 index 000000000..e5b2369e5 --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html @@ -0,0 +1,35 @@ + + + Select Server Backup + + + + + + + + + + + + +

+ Local Hostname + : {{ server.value.hostname }}.local +

+

+ StartOS Version + : {{ server.value.version }} +

+

+ Created + : {{ server.value.timestamp | date : 'medium' }} +

+
+
+
+
diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.scss similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss rename to web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.scss diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts new file mode 100644 index 000000000..a10067044 --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts @@ -0,0 +1,118 @@ +import { Component, Input } from '@angular/core' +import { ModalController, NavController } from '@ionic/angular' +import * as argon2 from '@start9labs/argon2' +import { + ErrorService, + LoadingService, + StartOSDiskInfo, +} from '@start9labs/shared' +import { + PasswordPromptComponent, + PromptOptions, +} from 'src/app/modals/password-prompt.component' +import { + BackupInfo, + CifsBackupTarget, + DiskBackupTarget, +} from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' +import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page' + +@Component({ + selector: 'backup-server-select', + templateUrl: 'backup-server-select.page.html', + styleUrls: ['backup-server-select.page.scss'], +}) +export class BackupServerSelectModal { + @Input() target!: MappedBackupTarget + + constructor( + private readonly modalCtrl: ModalController, + private readonly loader: LoadingService, + private readonly api: ApiService, + private readonly navCtrl: NavController, + private readonly errorService: ErrorService, + ) {} + + dismiss() { + this.modalCtrl.dismiss() + } + + async presentModalPassword( + serverId: string, + { passwordHash }: StartOSDiskInfo, + ): Promise { + const options: PromptOptions = { + title: 'Password Required', + message: + 'Enter the password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', + label: 'Decrypt Backup', + placeholder: 'Enter password', + buttonText: 'Next', + } + const modal = await this.modalCtrl.create({ + component: PasswordPromptComponent, + componentProps: { options }, + canDismiss: async password => { + if (password === null) { + return true + } + + try { + argon2.verify(passwordHash!, password) + await this.restoreFromBackup(serverId, password) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } + }, + }) + modal.present() + } + + private async restoreFromBackup( + serverId: string, + password: string, + ): Promise { + const loader = this.loader.open('Decrypting drive...').subscribe() + + try { + const backupInfo = await this.api.getBackupInfo({ + targetId: this.target.id, + serverId, + password, + }) + this.presentModalSelect(serverId, backupInfo, password) + } finally { + loader.unsubscribe() + } + } + + private async presentModalSelect( + serverId: string, + backupInfo: BackupInfo, + password: string, + ): Promise { + const modal = await this.modalCtrl.create({ + componentProps: { + targetId: this.target.id, + serverId, + backupInfo, + password, + }, + presentingElement: await this.modalCtrl.getTop(), + component: AppRecoverSelectPage, + }) + + modal.onDidDismiss().then(res => { + if (res.role === 'success') { + this.modalCtrl.dismiss(undefined, 'success') + this.navCtrl.navigateRoot('/services') + } + }) + + await modal.present() + } +} diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts b/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts deleted file mode 100644 index a0acb46d5..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { EnumListPage } from './enum-list.page' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [EnumListPage], - imports: [CommonModule, IonicModule, FormsModule], - exports: [EnumListPage], -}) -export class EnumListPageModule {} diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.html b/web/projects/ui/src/app/modals/enum-list/enum-list.page.html deleted file mode 100644 index 5cc74ba21..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.page.html +++ /dev/null @@ -1,45 +0,0 @@ - - - {{ spec.name }} - - - - - - - - - - - - - - {{ selectAll ? 'Select All' : 'Deselect All' }} - - - - - {{ spec.spec['value-names'][option.key] }} - - - - - - - - - - Done - - - - diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts b/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts deleted file mode 100644 index e5ddc8ed3..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { ValueSpecListOf } from 'src/app/pkg-config/config-types' - -@Component({ - selector: 'enum-list', - templateUrl: './enum-list.page.html', - styleUrls: ['./enum-list.page.scss'], -}) -export class EnumListPage { - @Input() key!: string - @Input() spec!: ValueSpecListOf<'enum'> - @Input() current: string[] = [] - - options: { [option: string]: boolean } = {} - selectAll = false - - constructor(private readonly modalCtrl: ModalController) {} - - ngOnInit() { - for (let val of this.spec.spec.values || []) { - this.options[val] = this.current.includes(val) - } - // if none are selected, set selectAll to true - this.selectAll = Object.values(this.options).some(k => !k) - } - - dismiss() { - this.modalCtrl.dismiss() - } - - save() { - this.modalCtrl.dismiss( - Object.keys(this.options).filter(key => this.options[key]), - ) - } - - toggleSelectAll() { - Object.keys(this.options).forEach(k => (this.options[k] = this.selectAll)) - this.selectAll = !this.selectAll - } - - toggleSelected(key: string) { - this.options[key] = !this.options[key] - } - - asIsOrder() { - return 0 - } -} diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts b/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts deleted file mode 100644 index f278f652b..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { GenericFormPage } from './generic-form.page' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module' - -@NgModule({ - declarations: [GenericFormPage], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - FormObjectComponentModule, - ], - exports: [GenericFormPage], -}) -export class GenericFormPageModule {} diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.html b/web/projects/ui/src/app/modals/generic-form/generic-form.page.html deleted file mode 100644 index 706c8487d..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - {{ title }} - - - - - - - - - -
- - -
-
- - - - - - {{ button.text }} - - - - diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss b/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss deleted file mode 100644 index 0353411b3..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss +++ /dev/null @@ -1,9 +0,0 @@ -button:disabled, -button[disabled]{ - border: 1px solid #999999; - background-color: #cccccc; - color: #666666; -} -button { - color: var(--ion-color-primary); -} \ No newline at end of file diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts b/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts deleted file mode 100644 index eb690a787..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, Input } from '@angular/core' -import { UntypedFormGroup } from '@angular/forms' -import { ModalController } from '@ionic/angular' -import { - convertValuesRecursive, - FormService, -} from 'src/app/services/form.service' -import { ConfigSpec } from 'src/app/pkg-config/config-types' - -export interface ActionButton { - text: string - handler: (value: any) => Promise - isSubmit?: boolean -} - -@Component({ - selector: 'generic-form', - templateUrl: './generic-form.page.html', - styleUrls: ['./generic-form.page.scss'], -}) -export class GenericFormPage { - @Input() title!: string - @Input() spec!: ConfigSpec - @Input() buttons!: ActionButton[] - @Input() initialValue: object = {} - - submitBtn!: ActionButton - formGroup!: UntypedFormGroup - - constructor( - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - ) {} - - ngOnInit() { - this.formGroup = this.formService.createForm(this.spec, this.initialValue) - this.submitBtn = this.buttons.find(btn => btn.isSubmit) || { - text: '', - handler: () => Promise.resolve(true), - } - } - - async dismiss(): Promise { - this.modalCtrl.dismiss() - } - - async handleClick(handler: ActionButton['handler']): Promise { - convertValuesRecursive(this.spec, this.formGroup) - - if (this.formGroup.invalid) { - document - .getElementsByClassName('validation-error')[0] - ?.scrollIntoView({ behavior: 'smooth' }) - return - } - - // @TODO make this more like generic input component dismissal - const success = await handler(this.formGroup.value) - if (success !== false) this.modalCtrl.dismiss() - } -} diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.html b/web/projects/ui/src/app/modals/generic-input/generic-input.component.html deleted file mode 100644 index 8ff7e795f..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.html +++ /dev/null @@ -1,67 +0,0 @@ - -
- - -

{{ options.title }}

-
-

{{ options.message }}

- -
-

- {{ options.warning }} -

-
-
-
- -
-
-

{{ options.label }}

- - - - - - - -

- {{ error }} -

-
- -
- Cancel - - {{ options.buttonText }} - -
-
-
-
diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts b/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts deleted file mode 100644 index d2b1faab4..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { GenericInputComponent } from './generic-input.component' -import { IonicModule } from '@ionic/angular' -import { RouterModule } from '@angular/router' -import { SharedPipesModule } from '@start9labs/shared' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [GenericInputComponent], - imports: [ - CommonModule, - IonicModule, - FormsModule, - RouterModule.forChild([]), - SharedPipesModule, - ], - exports: [GenericInputComponent], -}) -export class GenericInputComponentModule {} diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.scss b/web/projects/ui/src/app/modals/generic-input/generic-input.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts b/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts deleted file mode 100644 index 59ebb8c3d..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, inject, Input, ViewChild } from '@angular/core' -import { ModalController, IonicSafeString, IonInput } from '@ionic/angular' -import { getErrorMessage, THEME } from '@start9labs/shared' -import { MaskPipe } from 'src/app/pipes/mask/mask.pipe' - -@Component({ - selector: 'generic-input', - templateUrl: './generic-input.component.html', - styleUrls: ['./generic-input.component.scss'], - providers: [MaskPipe], -}) -export class GenericInputComponent { - @ViewChild('mainInput') elem?: IonInput - - @Input() options!: GenericInputOptions - - value!: string - masked!: boolean - - maskedValue?: string - - error: string | IonicSafeString = '' - - readonly theme$ = inject(THEME) - - constructor( - private readonly modalCtrl: ModalController, - private readonly mask: MaskPipe, - ) {} - - ngOnInit() { - const defaultOptions: Partial = { - buttonText: 'Submit', - placeholder: 'Enter value', - nullable: false, - useMask: false, - initialValue: '', - } - this.options = { - ...defaultOptions, - ...this.options, - } - - this.masked = !!this.options.useMask - this.value = this.options.initialValue || '' - } - - ngAfterViewInit() { - setTimeout(() => this.elem?.setFocus(), 400) - } - - toggleMask() { - this.masked = !this.masked - } - - cancel() { - this.modalCtrl.dismiss() - } - - transformInput(newValue: string) { - let i = 0 - this.value = newValue - .split('') - .map(x => (x === '●' ? this.value[i++] : x)) - .join('') - this.maskedValue = this.mask.transform(this.value) - } - - async submit() { - const value = this.value.trim() - - if (!value && !this.options.nullable) return - - try { - await this.options.submitFn(value) - this.modalCtrl.dismiss(undefined, 'success') - } catch (e: any) { - this.error = getErrorMessage(e) - } - } -} - -export interface GenericInputOptions { - // required - title: string - message: string - label: string - submitFn: (value: string) => Promise - // optional - warning?: string - buttonText?: string - placeholder?: string - nullable?: boolean - useMask?: boolean - initialValue?: string -} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts deleted file mode 100644 index 096e06add..000000000 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { MarketplaceSettingsPage } from './marketplace-settings.page' -import { SharedPipesModule } from '@start9labs/shared' -import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - SharedPipesModule, - StoreIconComponentModule, - ], - declarations: [MarketplaceSettingsPage], -}) -export class MarketplaceSettingsPageModule {} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html index 663b022cd..ab3bf8b34 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html @@ -1,67 +1,30 @@ - - - Change Registry - - - - - - - - - - - Default Registries - +

Default Registries

+ +

Custom Registries

+ +
+ + +
+
diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss index e69de29bb..310b9ab30 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss @@ -0,0 +1,5 @@ +.connect-container { + display: flex; + flex-direction: row; + align-items: center; +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts index 9743e531c..4c70d2837 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts @@ -1,276 +1,230 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { AbstractMarketplaceService } from '@start9labs/marketplace' import { - ChangeDetectionStrategy, - Component, - Inject, - ViewChild, -} from '@angular/core' + ErrorService, + LoadingService, + sameUrl, + toUrl, +} from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' import { - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActionSheetButton } from '@ionic/core' -import { ErrorToastService, sameUrl, toUrl } from '@start9labs/shared' -import { AbstractMarketplaceService } from '@start9labs/marketplace' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ValueSpecObject } from 'src/app/pkg-config/config-types' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' + TuiButtonModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, +} from '@taiga-ui/experimental' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { PatchDB } from 'patch-db-client' -import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' -import { MarketplaceService } from 'src/app/services/marketplace.service' +import { combineLatest, filter, firstValueFrom, Subscription } from 'rxjs' import { map } from 'rxjs/operators' -import { combineLatest, firstValueFrom } from 'rxjs' +import { FormComponent } from 'src/app/components/form.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { MarketplaceService } from 'src/app/services/marketplace.service' +import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' + +import { MarketplaceRegistryComponent } from './registry.component' @Component({ + standalone: true, + imports: [ + CommonModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, + TuiButtonModule, + MarketplaceRegistryComponent, + ], selector: 'marketplace-settings', templateUrl: 'marketplace-settings.page.html', styleUrls: ['marketplace-settings.page.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class MarketplaceSettingsPage { - stores$ = combineLatest([ - this.marketplaceService.getKnownHosts$(), - this.marketplaceService.getSelectedHost$(), + private readonly api = inject(ApiService) + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + private readonly formDialog = inject(FormDialogService) + private readonly dialogs = inject(TuiDialogService) + private readonly marketplace = inject( + AbstractMarketplaceService, + ) as MarketplaceService + private readonly hosts$ = inject(PatchDB).watch$( + 'ui', + 'marketplace', + 'knownHosts', + ) + + readonly stores$ = combineLatest([ + this.marketplace.getKnownHosts$(), + this.marketplace.getSelectedHost$(), ]).pipe( - map(([stores, selected]) => { - const toSlice = stores.map(s => ({ + map(([stores, selected]) => + stores.map(s => ({ ...s, selected: sameUrl(s.url, selected.url), - })) - // 0 and 1 are prod and community - const standard = toSlice.slice(0, 2) - // 2 and beyond are alts - const alt = toSlice.slice(2) - - return { standard, alt } - }), + })), + ), + // 0 and 1 are prod and community, 2 and beyond are alts + map(stores => ({ standard: stores.slice(0, 2), alt: stores.slice(2) })), ) - constructor( - private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly actionCtrl: ActionSheetController, - @Inject(AbstractMarketplaceService) - private readonly marketplaceService: MarketplaceService, - private readonly patch: PatchDB, - private readonly alertCtrl: AlertController, - ) {} - - async dismiss() { - this.modalCtrl.dismiss() - } - - async presentModalAdd() { + async add() { const { name, spec } = getMarketplaceValueSpec() - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: name, + + this.formDialog.open(FormComponent, { + label: name, + data: { spec, buttons: [ { text: 'Save for Later', - handler: (value: { url: string }) => { - this.saveOnly(value.url) - }, + handler: async ({ url }: { url: string }) => this.save(url), }, { text: 'Save and Connect', - handler: (value: { url: string }) => { - this.saveAndConnect(value.url) - }, + handler: async ({ url }: { url: string }) => this.save(url, true), isSubmit: true, }, ], }, - cssClass: 'alertlike-modal', }) - - await modal.present() } - - async presentAction( - { url, name }: { url: string; name?: string }, - canDelete = false, - ) { - const buttons: ActionSheetButton[] = [ - { - text: 'Connect', - handler: () => { - this.connect(url) - }, - }, - ] - - if (canDelete) { - buttons.unshift({ - text: 'Delete', - role: 'destructive', - handler: () => { - this.presentAlertDelete(url, name!) - }, + delete(url: string, name: string = '') { + this.dialogs + .open(TUI_PROMPT, getPromptOptions(name)) + .pipe(filter(Boolean)) + .subscribe(async () => { + const loader = this.loader.open('Deleting...').subscribe() + const hosts = await firstValueFrom(this.hosts$) + const filtered: { [url: string]: UIStore } = Object.keys(hosts) + .filter(key => !sameUrl(key, url)) + .reduce( + (prev, curr) => ({ + ...prev, + [curr]: hosts[curr], + }), + {}, + ) + + try { + await this.api.setDbValue(['marketplace', 'knownHosts'], filtered) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } }) - } - - const action = await this.actionCtrl.create({ - header: name, - mode: 'ios', - buttons, - }) - - await action.present() - } - - private async presentAlertDelete(url: string, name: string) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to delete ${name}?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => this.delete(url), - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() } - private async connect( + async connect( url: string, - loader?: HTMLIonLoadingElement, + loader: Subscription = new Subscription(), ): Promise { - const message = 'Changing Registry...' - if (!loader) { - loader = await this.loadingCtrl.create({ message }) - await loader.present() - } else { - loader.message = message - } + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Changing Registry...').subscribe()) try { - await this.api.setDbValue(['marketplace', 'selected-url'], url) + await this.api.setDbValue(['marketplace', 'selectedUrl'], url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } - private async saveOnly(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() + private async save(rawUrl: string, connect = false): Promise { + const loader = this.loader.open('Loading').subscribe() + const url = new URL(rawUrl).toString() try { - const url = new URL(rawUrl).toString() await this.validateAndSave(url, loader) + if (connect) await this.connect(url, loader) + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) + return false } finally { - loader.dismiss() - } - } - - private async saveAndConnect(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() - - try { - const url = new URL(rawUrl).toString() - await this.validateAndSave(url, loader) - await this.connect(url, loader) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } private async validateAndSave( url: string, - loader: HTMLIonLoadingElement, + loader: Subscription, ): Promise { // Error on duplicates - const hosts = await firstValueFrom( - this.patch.watch$('ui', 'marketplace', 'known-hosts'), - ) + const hosts = await firstValueFrom(this.hosts$) const currentUrls = Object.keys(hosts).map(toUrl) - if (currentUrls.includes(url)) throw new Error('marketplace already added') + if (currentUrls.includes(url)) throw new Error('Marketplace already added') // Validate - loader.message = 'Validating marketplace...' - await loader.present() + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Validating marketplace...').subscribe()) - const { name } = await firstValueFrom( - this.marketplaceService.fetchInfo$(url), - ) + const { name } = await firstValueFrom(this.marketplace.fetchInfo$(url)) // Save - loader.message = 'Saving...' + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Saving...').subscribe()) - await this.api.setDbValue<{ name: string }>( - ['marketplace', 'known-hosts', url], - { name }, - ) - } - - private async delete(url: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() - - const hosts = await firstValueFrom( - this.patch.watch$('ui', 'marketplace', 'known-hosts'), - ) - - const filtered: { [url: string]: UIStore } = Object.keys(hosts) - .filter(key => !sameUrl(key, url)) - .reduce((prev, curr) => { - const name = hosts[curr] - return { - ...prev, - [curr]: name, - } - }, {}) - - try { - await this.api.setDbValue<{ [url: string]: UIStore }>( - ['marketplace', 'known-hosts'], - filtered, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } + await this.api.setDbValue(['marketplace', 'knownHosts', url], { name }) } } -function getMarketplaceValueSpec(): ValueSpecObject { +export const MARKETPLACE_REGISTRY = new PolymorpheusComponent( + MarketplaceSettingsPage, +) + +function getMarketplaceValueSpec(): IST.ValueSpecObject { return { type: 'object', name: 'Add Custom Registry', + description: null, + warning: null, spec: { url: { - type: 'string', + type: 'text', name: 'URL', description: 'A fully-qualified URL of the custom registry', - nullable: false, + inputmode: 'url', + required: true, masked: false, - copyable: false, - pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`, - 'pattern-description': 'Must be a valid URL', + minLength: null, + maxLength: null, + patterns: [ + { + regex: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`, + description: 'Must be a valid URL', + }, + ], placeholder: 'e.g. https://example.org', + default: null, + warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } } + +function getPromptOptions( + name: string, +): Partial> { + return { + label: 'Confirm', + size: 's', + data: { + content: `Are you sure you want to delete ${name}?`, + yes: 'Delete', + no: 'Cancel', + }, + } +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts b/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts new file mode 100644 index 000000000..0441ae18f --- /dev/null +++ b/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts @@ -0,0 +1,42 @@ +import { NgIf } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental' +import { ConfigService } from 'src/app/services/config.service' + +import { StoreIconComponent } from './store-icon.component' + +@Component({ + standalone: true, + selector: '[registry]', + template: ` + +
+ {{ registry.name }} +
{{ registry.url }}
+
+ + + `, + styles: [':host { border-radius: 0.25rem; width: stretch; }'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgIf, StoreIconComponent, TuiIconModule, TuiTitleModule], +}) +export class MarketplaceRegistryComponent { + readonly marketplace = inject(ConfigService).marketplace + + @Input() + registry!: { url: string; selected: boolean; name?: string } +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts b/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts new file mode 100644 index 000000000..dcdfe3b2f --- /dev/null +++ b/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts @@ -0,0 +1,46 @@ +import { NgIf } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { sameUrl } from '@start9labs/shared' + +@Component({ + standalone: true, + selector: 'store-icon', + template: ` + Marketplace Icon + + Marketplace Icon + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgIf], +}) +export class StoreIconComponent { + @Input() + url = '' + @Input() + size?: string + @Input() + marketplace!: any + + get icon() { + const { start9, community } = this.marketplace + + if (sameUrl(this.url, start9)) { + return 'assets/img/icon_transparent.png' + } else if (sameUrl(this.url, community)) { + return 'assets/img/community-store.png' + } + return null + } +} diff --git a/web/projects/ui/src/app/modals/os-update/os-update.page.ts b/web/projects/ui/src/app/modals/os-update/os-update.page.ts index eb9de93db..45b9cb009 100644 --- a/web/projects/ui/src/app/modals/os-update/os-update.page.ts +++ b/web/projects/ui/src/app/modals/os-update/os-update.page.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ApiService } from '../../services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { ModalController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' +import { ApiService } from '../../services/api/embassy-api.service' @Component({ selector: 'os-update', @@ -15,14 +15,13 @@ export class OSUpdatePage { constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly eosService: EOSService, ) {} - ngOnInit() { - const releaseNotes = this.eosService.eos?.['release-notes']! + const releaseNotes = this.eosService.osUpdate?.releaseNotes! this.versions = Object.keys(releaseNotes) .sort() @@ -40,18 +39,15 @@ export class OSUpdatePage { } async updateEOS() { - const loader = await this.loadingCtrl.create({ - message: 'Beginning update...', - }) - await loader.present() + const loader = this.loader.open('Beginning update...').subscribe() try { await this.embassyApi.updateServer() this.dismiss() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.module.ts b/web/projects/ui/src/app/modals/os-welcome/os-welcome.module.ts deleted file mode 100644 index 3e910403e..000000000 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { OSWelcomePage } from './os-welcome.page' -import { SharedPipesModule } from '@start9labs/shared' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [OSWelcomePage], - imports: [CommonModule, IonicModule, FormsModule, SharedPipesModule], - exports: [OSWelcomePage], -}) -export class OSWelcomePageModule {} diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html deleted file mode 100644 index 23bc7e1fd..000000000 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ /dev/null @@ -1,73 +0,0 @@ - - - Release Notes - - - - - - - - - -

This Release

- -

0.3.5.1

-

- View the complete - - release notes - - for more details. -

-
Highlights
-
    -
  • Revert perpetual performance mode for quieter fan
  • -
  • Minor bug fixes
  • -
- -

Previous 0.3.5.x Releases

- -

0.3.5

-

- View the complete - - release notes - - for more details. -

-
Highlights
-
    -
  • - This release contains significant under-the-hood improvements to - performance and reliability -
  • -
  • Ditch Docker, replace with Podman
  • -
  • Remove locking behavior from PatchDB and optimize
  • -
  • Boost efficiency of service manager
  • -
  • Require HTTPS on LAN, and improve setup flow for trusting Root CA
  • -
  • Better default privacy settings for Firefox kiosk mode
  • -
  • Eliminate memory leak from Javascript runtime
  • -
  • Other small bug fixes
  • -
  • Update license to MIT
  • -
- -
- - Begin - -
-
diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.scss b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.scss deleted file mode 100644 index 0dc939f99..000000000 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.scss +++ /dev/null @@ -1,29 +0,0 @@ -.close-button { - width: 100%; - display: flex; - justify-content: center; - align-items: center; - min-height: 100px; -} - -.main-content { - color: var(--ion-color-dark); -} - -.spaced-list { - li { - padding-bottom: 12px; - } -} - -.note-padding { - padding-bottom: 12px; -} - -h2 { - font-weight: bold; -} - -h4 { - font-style: italic; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.ts b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.ts deleted file mode 100644 index f9a6ecd7b..000000000 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' - -@Component({ - selector: 'os-welcome', - templateUrl: './os-welcome.page.html', - styleUrls: ['./os-welcome.page.scss'], -}) -export class OSWelcomePage { - constructor(private readonly modalCtrl: ModalController) {} - - async dismiss() { - return this.modalCtrl.dismiss() - } -} diff --git a/web/projects/ui/src/app/modals/password-prompt.component.ts b/web/projects/ui/src/app/modals/password-prompt.component.ts new file mode 100644 index 000000000..a9fec75cd --- /dev/null +++ b/web/projects/ui/src/app/modals/password-prompt.component.ts @@ -0,0 +1,94 @@ +import { + AfterViewInit, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IonicModule, ModalController } from '@ionic/angular' +import { TuiTextfieldComponent } from '@taiga-ui/core' +import { TuiInputPasswordModule } from '@taiga-ui/kit' + +export interface PromptOptions { + title: string + message: string + label: string + placeholder: string + buttonText: string +} + +@Component({ + standalone: true, + template: ` + + + {{ options.title }} + + + + + + + + + +

{{ options.message }}

+

+ + {{ options.label }} + + +

+
+ + + + + Cancel + + + {{ options.buttonText }} + + + + `, + imports: [IonicModule, FormsModule, TuiInputPasswordModule], +}) +export class PasswordPromptComponent implements AfterViewInit { + @ViewChild(TuiTextfieldComponent, { read: ElementRef }) + input?: ElementRef + + @Input() + options!: PromptOptions + + password = '' + + constructor(private modalCtrl: ModalController) {} + + ngAfterViewInit() { + setTimeout(() => { + this.input?.nativeElement.focus({ preventScroll: true }) + }, 300) + } + + cancel() { + return this.modalCtrl.dismiss(null, 'cancel') + } + + confirm() { + return this.modalCtrl.dismiss(this.password, 'confirm') + } +} diff --git a/web/projects/ui/src/app/modals/prompt.component.ts b/web/projects/ui/src/app/modals/prompt.component.ts new file mode 100644 index 000000000..e2a2765f5 --- /dev/null +++ b/web/projects/ui/src/app/modals/prompt.component.ts @@ -0,0 +1,123 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { TuiAutoFocusModule } from '@taiga-ui/cdk' +import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiInputModule } from '@taiga-ui/kit' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusComponent, +} from '@tinkoff/ng-polymorpheus' + +@Component({ + standalone: true, + template: ` +

{{ options.message }}

+

{{ options.warning }}

+
+ + {{ options.label }} + * + + +
+ + +
+
+ + + + + `, + styles: [ + ` + .warning { + color: var(--tui-warning-fill); + } + + .button { + pointer-events: auto; + margin-left: 0.25rem; + } + + .masked { + -webkit-text-security: disc; + } + `, + ], + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiButtonModule, + TuiTextfieldControllerModule, + TuiAutoFocusModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PromptModal { + masked = this.options.useMask + value = this.options.initialValue || '' + + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + ) {} + + get options(): PromptOptions { + return this.context.data + } + + cancel() { + this.context.$implicit.complete() + } + + submit(value: string) { + if (value || !this.options.required) { + this.context.$implicit.next(value) + } + } +} + +export const PROMPT = new PolymorpheusComponent(PromptModal) + +export interface PromptOptions { + message: string + label?: string + warning?: string + buttonText?: string + placeholder?: string + required?: boolean + useMask?: boolean + initialValue?: string | null +} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html index 71bcc6e40..11faa8235 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html @@ -1,7 +1,10 @@ - - + +

{{ action.name }}

{{ action.description }}

+

+ {{ disabledText }} +

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts index 5f30abc0e..377504238 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts @@ -5,8 +5,6 @@ import { IonicModule } from '@ionic/angular' import { AppActionsPage, AppActionsItemComponent } from './app-actions.page' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { SharedPipesModule } from '@start9labs/shared' -import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' -import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module' const routes: Routes = [ { @@ -22,8 +20,6 @@ const routes: Routes = [ RouterModule.forChild(routes), QRComponentModule, SharedPipesModule, - GenericFormPageModule, - ActionSuccessPageModule, ], declarations: [AppActionsPage, AppActionsItemComponent], }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index adcfcb829..3532fcacc 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -11,27 +11,34 @@ Standard Actions + - + Actions for {{ pkg.manifest.title }} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index bd25a1a42..949a1adad 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -1,23 +1,19 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' import { - AlertController, - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { getPkgId } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' import { PatchDB } from 'patch-db-client' -import { - Action, - DataModel, - PackageDataEntry, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' -import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' -import { hasCurrentDeps } from 'src/app/util/has-deps' +import { ActionService } from 'src/app/services/action.service' +import { StandardActionsService } from 'src/app/services/standard-actions.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { getManifest } from 'src/app/util/get-package-data' +import { filter, map } from 'rxjs' @Component({ selector: 'app-actions', @@ -27,194 +23,47 @@ import { hasCurrentDeps } from 'src/app/util/has-deps' }) export class AppActionsPage { readonly pkgId = getPkgId(this.route) - readonly pkg$ = this.patch.watch$('package-data', this.pkgId) + readonly pkg$ = this.patch.watch$('packageData', this.pkgId).pipe( + filter(pkg => pkg.stateInfo.state === 'installed'), + map(pkg => ({ + mainStatus: pkg.status.main, + icon: pkg.icon, + manifest: getManifest(pkg), + actions: Object.keys(pkg.actions).map(id => ({ + id, + ...pkg.actions[id], + })), + })), + ) constructor( private readonly route: ActivatedRoute, - private readonly embassyApi: ApiService, - private readonly modalCtrl: ModalController, - private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, - private readonly navCtrl: NavController, private readonly patch: PatchDB, + private readonly actionService: ActionService, + private readonly standardActionsService: StandardActionsService, ) {} async handleAction( - pkg: PackageDataEntry, - action: { key: string; value: Action }, + mainStatus: T.MainStatus['main'], + icon: string, + manifest: T.Manifest, + action: T.ActionMetadata & { id: string }, ) { - const status = pkg.installed?.status - if ( - status && - (action.value['allowed-statuses'] as PackageMainStatus[]).includes( - status.main.status, - ) - ) { - if (!isEmptyObject(action.value['input-spec'] || {})) { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: action.value.name, - spec: action.value['input-spec'], - buttons: [ - { - text: 'Execute', - handler: (value: any) => { - return this.executeAction(action.key, value) - }, - isSubmit: true, - }, - ], - }, - }) - await modal.present() - } else { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to execute action "${ - action.value.name - }"? ${action.value.warning || ''}`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Execute', - handler: () => { - this.executeAction(action.key) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - } else { - const statuses = [...action.value['allowed-statuses']] - const last = statuses.pop() - let statusesStr = statuses.join(', ') - let error = '' - if (statuses.length) { - if (statuses.length > 1) { - // oxford comma - statusesStr += ',' - } - statusesStr += ` or ${last}` - } else if (last) { - statusesStr = `${last}` - } else { - error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.` - } - const alert = await this.alertCtrl.create({ - header: 'Forbidden', - message: - error || - `Action "${action.value.name}" can only be executed when service is ${statusesStr}`, - buttons: ['OK'], - cssClass: 'alert-error-message enter-click', - }) - await alert.present() - } - } - - async tryUninstall(pkg: PackageDataEntry): Promise { - const { title, alerts } = pkg.manifest - - let message = - alerts.uninstall || - `Uninstalling ${title} will permanently delete its data` - - if (hasCurrentDeps(pkg)) { - message = `${message}. Services that depend on ${title} will no longer work properly and may crash` - } - - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Uninstall', - handler: () => { - this.uninstall() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', + this.actionService.present({ + pkgInfo: { id: manifest.id, title: manifest.title, icon, mainStatus }, + actionInfo: { id: action.id, metadata: action }, }) - - await alert.present() } - private async uninstall() { - const loader = await this.loadingCtrl.create({ - message: `Beginning uninstall...`, - }) - await loader.present() - - try { - await this.embassyApi.uninstallPackage({ id: this.pkgId }) - this.embassyApi - .setDbValue(['ack-instructions', this.pkgId], false) - .catch(e => console.error('Failed to mark instructions as unseen', e)) - this.navCtrl.navigateRoot('/services') - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } + async rebuild(id: string) { + return this.standardActionsService.rebuild(id) } - private async executeAction( - actionId: string, - input?: object, - ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Executing action...', - }) - await loader.present() - - try { - const res = await this.embassyApi.executePackageAction({ - id: this.pkgId, - 'action-id': actionId, - input, - }) - - const successModal = await this.modalCtrl.create({ - component: ActionSuccessPage, - componentProps: { - actionRes: res, - }, - }) - - setTimeout(() => successModal.present(), 500) - return true // needed to dismiss original modal/alert - } catch (e: any) { - this.errToast.present(e) - return false // don't dismiss original modal/alert - } finally { - loader.dismiss() - } - } - - asIsOrder() { - return 0 + async tryUninstall(manifest: T.Manifest) { + return this.standardActionsService.tryUninstall(manifest) } } -interface LocalAction { - name: string - description: string - icon: string -} - @Component({ selector: 'app-actions-item', templateUrl: './app-actions-item.component.html', @@ -222,5 +71,19 @@ interface LocalAction { changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppActionsItemComponent { - @Input() action!: LocalAction + @Input() action!: { + name: string + description: string + visibility: T.ActionVisibility + } + @Input() icon!: string + + @Output() onClick: EventEmitter = new EventEmitter() + + get disabledText() { + return ( + typeof this.action.visibility === 'object' && + this.action.visibility.disabled + ) + } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html deleted file mode 100644 index 932c1fd0a..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -

{{ interface.def.name }}

-

{{ interface.def.description }}

-
-
-
- - - -

Tor Address

-

{{ tor }}

-
- - - - - - - - - - - -
- - - -

Tor Address

-

Service does not use a Tor Address

-
-
- - - - -

LAN Address

-

{{ lan }}

-
- - - - - - - - - - - -
- - - -

LAN Address

-

N/A

-
-
-
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts index a9a2ddebc..0300e4e70 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts @@ -3,11 +3,8 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { SharedPipesModule } from '@start9labs/shared' - -import { - AppInterfacesItemComponent, - AppInterfacesPage, -} from './app-interfaces.page' +import { AppInterfacesPage } from './app-interfaces.page' +import { InterfaceInfoModule } from 'src/app/components/interface-info/interface-info.module' const routes: Routes = [ { @@ -22,7 +19,8 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharedPipesModule, + InterfaceInfoModule, ], - declarations: [AppInterfacesPage, AppInterfacesItemComponent], + declarations: [AppInterfacesPage], }) export class AppInterfacesPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index 16c6a5bd6..7093756a8 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -8,19 +8,34 @@ - - - - User Interface - + + + User Interfaces + - - - Machine Interfaces -
- -
+ + Application Program Interfaces + + + + + Peer-To-Peer Interfaces +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.scss b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.scss index 61ead3b94..e69de29bb 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.scss +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.scss @@ -1,3 +0,0 @@ -p { - font-family: 'Courier New'; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 825d6536d..2feb03963 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -1,22 +1,11 @@ -import { Component, Inject, Input } from '@angular/core' -import { WINDOW } from '@ng-web-apis/common' +import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController, ToastController } from '@ionic/angular' -import { copyToClipboard, getPkgId } from '@start9labs/shared' -import { getUiInterfaceKey } from 'src/app/services/config.service' -import { - DataModel, - InstalledPackageDataEntry, - InterfaceDef, -} from 'src/app/services/patch-db/data-model' +import { getPkgId } from '@start9labs/shared' +import { DataModel } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' -import { QRComponent } from 'src/app/components/qr/qr.component' -import { getPackage } from '../../../util/get-package-data' - -interface LocalInterface { - def: InterfaceDef - addresses: InstalledPackageDataEntry['interface-addresses'][string] -} +import { combineLatest, map } from 'rxjs' +import { getAddresses } from 'src/app/components/interface-info/interface-info.component' +import { ConfigService } from 'src/app/services/config.service' @Component({ selector: 'app-interfaces', @@ -24,105 +13,48 @@ interface LocalInterface { styleUrls: ['./app-interfaces.page.scss'], }) export class AppInterfacesPage { - ui?: LocalInterface - other: LocalInterface[] = [] readonly pkgId = getPkgId(this.route) - constructor( - private readonly route: ActivatedRoute, - private readonly patch: PatchDB, - ) {} - - async ngOnInit() { - const pkg = await getPackage(this.patch, this.pkgId) - if (!pkg) return - - const interfaces = pkg.manifest.interfaces - const uiKey = getUiInterfaceKey(interfaces) - - if (!pkg.installed) return - - const addressesMap = pkg.installed['interface-addresses'] - - if (uiKey) { - const uiAddresses = addressesMap[uiKey] - this.ui = { - def: interfaces[uiKey], - addresses: { - 'lan-address': uiAddresses['lan-address'] - ? 'https://' + uiAddresses['lan-address'] - : '', - // leave http for services - 'tor-address': uiAddresses['tor-address'] - ? 'http://' + uiAddresses['tor-address'] - : '', - }, + private readonly serviceInterfaces$ = this.patch.watch$( + 'packageData', + this.pkgId, + 'serviceInterfaces', + ) + private readonly hosts$ = this.patch.watch$( + 'packageData', + this.pkgId, + 'hosts', + ) + + readonly serviceInterfacesWithHostInfo$ = combineLatest([ + this.serviceInterfaces$, + this.hosts$, + ]).pipe( + map(([interfaces, hosts]) => { + const sorted = Object.values(interfaces) + .sort(iface => + iface.name.toLowerCase() > iface.name.toLowerCase() ? -1 : 1, + ) + .map(iface => { + const host = hosts[iface.addressInfo.hostId] + return { + ...iface, + public: host.bindings[iface.addressInfo.internalPort].net.public, + addresses: getAddresses(iface, host, this.config), + } + }) + + return { + ui: sorted.filter(val => val.type === 'ui'), + api: sorted.filter(val => val.type === 'api'), + p2p: sorted.filter(val => val.type === 'p2p'), } - } - - this.other = Object.keys(interfaces) - .filter(key => key !== uiKey) - .map(key => { - const addresses = addressesMap[key] - return { - def: interfaces[key], - addresses: { - 'lan-address': addresses['lan-address'] - ? 'https://' + addresses['lan-address'] - : '', - 'tor-address': addresses['tor-address'] - ? // leave http for services - 'http://' + addresses['tor-address'] - : '', - }, - } - }) - } -} - -@Component({ - selector: 'app-interfaces-item', - templateUrl: './app-interfaces-item.component.html', - styleUrls: ['./app-interfaces.page.scss'], -}) -export class AppInterfacesItemComponent { - @Input() - interface!: LocalInterface + }), + ) constructor( - private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, - @Inject(WINDOW) private readonly windowRef: Window, + private readonly route: ActivatedRoute, + private readonly patch: PatchDB, + private readonly config: ConfigService, ) {} - - launch(url: string): void { - this.windowRef.open(url, '_blank', 'noreferrer') - } - - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - async copy(address: string): Promise { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html index 8f1af1470..b0bfbbb24 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html @@ -1,4 +1,4 @@ - + - +

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts index 3302e182e..fdc0a3b2a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { PackageMainStatus } from 'src/app/services/patch-db/data-model' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PkgInfo } from 'src/app/util/get-package-info' import { UiLauncherService } from 'src/app/services/ui-launcher.service' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-list-pkg', @@ -14,15 +15,21 @@ export class AppListPkgComponent { constructor(private readonly launcherService: UiLauncherService) {} - get status(): PackageMainStatus { - return ( - this.pkg.entry.installed?.status.main.status || PackageMainStatus.Stopped - ) + get pkgMainStatus(): T.MainStatus['main'] { + return this.pkg.entry.status.main } - launchUi(e: Event): void { + get sigtermTimeout(): string | null { + return this.pkgMainStatus === 'stopping' ? '30s' : null // @dr-bonez TODO + } + + launchUi( + e: Event, + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { e.stopPropagation() e.preventDefault() - this.launcherService.launch(this.pkg.entry) + this.launcherService.launch(interfaces, hosts) } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts index 89998c9bf..aa4c5fcd6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppListPage } from './app-list.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, TextSpinnerComponentModule, TickerModule, @@ -29,7 +29,7 @@ const routes: Routes = [ imports: [ CommonModule, StatusComponentModule, - EmverPipesModule, + ExverPipesModule, TextSpinnerComponentModule, LaunchablePipeModule, UiPipeModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html index 3bc38f762..cc19bdff5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -27,7 +27,7 @@

Welcome to StartOS

sizeMd="6" > diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index f7d7685ff..8c36d22dc 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { filter, map, pairwise, startWith } from 'rxjs/operators' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'app-list', @@ -10,7 +11,7 @@ import { filter, map, pairwise, startWith } from 'rxjs/operators' changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppListPage { - readonly pkgs$ = this.patch.watch$('package-data').pipe( + readonly pkgs$ = this.patch.watch$('packageData').pipe( map(pkgs => Object.values(pkgs)), startWith([]), pairwise(), @@ -20,7 +21,7 @@ export class AppListPage { }), map(([_, pkgs]) => pkgs.sort((a, b) => - b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase() + getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase() ? -1 : 1, ), diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts index c0d00c2c9..3695c4536 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' -import { Observable, combineLatest, firstValueFrom } from 'rxjs' +import { Observable, combineLatest } from 'rxjs' import { filter, map } from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' import { getPackageInfo, PkgInfo } from '../../../util/get-package-info' @@ -17,7 +17,7 @@ export class PackageInfoPipe implements PipeTransform { transform(pkgId: string): Observable { return combineLatest([ - this.patch.watch$('package-data', pkgId).pipe(filter(Boolean)), + this.patch.watch$('packageData', pkgId).pipe(filter(Boolean)), this.depErrorService.getPkgDepErrors$(pkgId), ]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors))) } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts deleted file mode 100644 index 2c53d0fea..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { AppMetricsPage } from './app-metrics.page' -import { SharedPipesModule } from '@start9labs/shared' -import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' - -const routes: Routes = [ - { - path: '', - component: AppMetricsPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - SharedPipesModule, - SkeletonListComponentModule, - ], - declarations: [AppMetricsPage], -}) -export class AppMetricsPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html deleted file mode 100644 index cca899f46..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Monitor - - - - - - - - - - - {{ metric.key }} - - - {{ metric.value.value }} {{ metric.value.unit }} - - - - - diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss deleted file mode 100644 index eea898305..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -.metric-note { - font-size: 16px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts deleted file mode 100644 index 8cf576bd7..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { Metric } from 'src/app/services/api/api.types' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared' - -@Component({ - selector: 'app-metrics', - templateUrl: './app-metrics.page.html', - styleUrls: ['./app-metrics.page.scss'], -}) -export class AppMetricsPage { - loading = true - readonly pkgId = getPkgId(this.route) - going = false - metrics?: Metric - - constructor( - private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, - private readonly embassyApi: ApiService, - ) {} - - ngOnInit() { - this.startDaemon() - } - - ngOnDestroy() { - this.stopDaemon() - } - - async startDaemon(): Promise { - this.going = true - while (this.going) { - const startTime = Date.now() - await this.getMetrics() - await pauseFor(Math.max(4000 - (Date.now() - startTime), 0)) - } - } - - stopDaemon() { - this.going = false - } - - async getMetrics(): Promise { - try { - this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId }) - } catch (e: any) { - this.errToast.present(e) - this.stopDaemon() - } finally { - this.loading = false - } - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts deleted file mode 100644 index 2d8553017..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { AppPropertiesPage } from './app-properties.page' -import { QRComponentModule } from 'src/app/components/qr/qr.component.module' -import { MaskPipeModule } from 'src/app/pipes/mask/mask.module' -import { - SharedPipesModule, - TextSpinnerComponentModule, -} from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: AppPropertiesPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - QRComponentModule, - SharedPipesModule, - TextSpinnerComponentModule, - MaskPipeModule, - ], - declarations: [AppPropertiesPage], -}) -export class AppPropertiesPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html deleted file mode 100644 index ca3cdd3be..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Properties - - - - Refresh - - - - - - - - - - - - -

- - Service is stopped. Information on this page could be inaccurate. - -

-
-
- - - - -

No properties.

-
-
- - - -
- - - - - - -

{{ prop.key }}

-
-
- - - - - - -

{{ prop.key }}

-

- {{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | - mask : 64) : prop.value.value }} -

-
-
- - - - - - - - - -
-
-
-
-
-
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss deleted file mode 100644 index eea898305..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -.metric-note { - font-size: 16px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts deleted file mode 100644 index 8a49bad52..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Component, ViewChild } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - AlertController, - IonBackButtonDelegate, - ModalController, - NavController, - ToastController, -} from '@ionic/angular' -import { PackageProperties } from 'src/app/util/properties.util' -import { QRComponent } from 'src/app/components/qr/qr.component' -import { PatchDB } from 'patch-db-client' -import { - DataModel, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' -import { - ErrorToastService, - getPkgId, - copyToClipboard, -} from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { getValueByPointer } from 'fast-json-patch' -import { map, takeUntil } from 'rxjs/operators' - -@Component({ - selector: 'app-properties', - templateUrl: './app-properties.page.html', - styleUrls: ['./app-properties.page.scss'], - providers: [TuiDestroyService], -}) -export class AppPropertiesPage { - loading = true - readonly pkgId = getPkgId(this.route) - - pointer = '' - node: PackageProperties = {} - - properties: PackageProperties = {} - unmasked: { [key: string]: boolean } = {} - - stopped$ = this.patch - .watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status') - .pipe(map(status => status === PackageMainStatus.Stopped)) - - @ViewChild(IonBackButtonDelegate, { static: false }) - backButton?: IonBackButtonDelegate - - constructor( - private readonly route: ActivatedRoute, - private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, - private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, - private readonly navCtrl: NavController, - private readonly patch: PatchDB, - private readonly destroy$: TuiDestroyService, - ) {} - - ionViewDidEnter() { - if (!this.backButton) return - this.backButton.onClick = () => { - history.back() - } - } - - async ngOnInit() { - await this.getProperties() - - this.route.queryParams - .pipe(takeUntil(this.destroy$)) - .subscribe(queryParams => { - if (queryParams['pointer'] === this.pointer) return - this.pointer = queryParams['pointer'] || '' - this.node = getValueByPointer(this.properties, this.pointer) - }) - } - - async refresh() { - await this.getProperties() - } - - async presentDescription( - property: { key: string; value: PackageProperties[''] }, - e: Event, - ) { - e.stopPropagation() - - const alert = await this.alertCtrl.create({ - header: property.key, - message: property.value.description || undefined, - }) - await alert.present() - } - - async goToNested(key: string): Promise { - this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, { - queryParams: { - pointer: `${this.pointer}/${key}/value`, - }, - }) - } - - async copy(text: string): Promise { - let message = '' - await copyToClipboard(text).then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - toggleMask(key: string) { - this.unmasked[key] = !this.unmasked[key] - } - - private async getProperties(): Promise { - this.loading = true - try { - this.properties = await this.embassyApi.getPackageProperties({ - id: this.pkgId, - }) - this.node = getValueByPointer(this.properties, this.pointer) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.loading = false - } - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index c69910c78..5787948d6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -3,9 +3,12 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' -import { EmverPipesModule, ResponsiveColModule } from '@start9labs/shared' +import { + ExverPipesModule, + ResponsiveColModule, + SharedPipesModule, +} from '@start9labs/shared' import { StatusComponentModule } from 'src/app/components/status/status.component.module' -import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module' import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module' import { UiPipeModule } from 'src/app/pipes/ui/ui.module' import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component' @@ -15,10 +18,12 @@ import { AppShowDependenciesComponent } from './components/app-show-dependencies import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.component' import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component' import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component' +import { AppShowErrorComponent } from './components/app-show-error/app-show-error.component' +import { AppShowActionRequestsComponent } from './components/app-show-action-requests/app-show-action-requests.component' import { HealthColorPipe } from './pipes/health-color.pipe' import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe' import { ToButtonsPipe } from './pipes/to-buttons.pipe' -import { ProgressDataPipe } from './pipes/progress-data.pipe' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' const routes: Routes = [ { @@ -31,7 +36,6 @@ const routes: Routes = [ declarations: [ AppShowPage, HealthColorPipe, - ProgressDataPipe, ToHealthChecksPipe, ToButtonsPipe, AppShowHeaderComponent, @@ -41,17 +45,21 @@ const routes: Routes = [ AppShowMenuComponent, AppShowHealthChecksComponent, AppShowAdditionalComponent, + AppShowErrorComponent, + AppShowActionRequestsComponent, ], imports: [ CommonModule, - StatusComponentModule, + InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), - AppConfigPageModule, - EmverPipesModule, + ExverPipesModule, LaunchablePipeModule, UiPipeModule, ResponsiveColModule, + StatusComponentModule, + SharedPipesModule, ], + exports: [AppShowProgressComponent], }) export class AppShowPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index d2b7eaead..ef99965b5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -7,9 +7,7 @@ @@ -18,23 +16,39 @@ - - + + + + + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index ab250e1a7..42b9d3c8a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -2,28 +2,31 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { + AllPackageData, DataModel, - InstalledPackageDataEntry, - Manifest, + InstallingState, PackageDataEntry, - PackageState, + UpdatingState, } from 'src/app/services/patch-db/data-model' -import { - PackageStatus, - PrimaryStatus, - renderPkgStatus, -} from 'src/app/services/pkg-status-rendering.service' +import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { map, tap } from 'rxjs/operators' import { ActivatedRoute, NavigationExtras } from '@angular/router' import { getPkgId } from '@start9labs/shared' -import { ModalService } from 'src/app/services/modal.service' import { DependentInfo } from 'src/app/types/dependent-info' import { DepErrorService, - DependencyErrorType, PkgDependencyErrors, } from 'src/app/services/dep-error.service' import { combineLatest } from 'rxjs' +import { + getManifest, + isInstalled, + isInstalling, + isRestoring, + isUpdating, +} from 'src/app/util/get-package-data' +import { T } from '@start9labs/start-sdk' +import { getDepDetails } from 'src/app/util/dep-info' export interface DependencyInfo { id: string @@ -31,137 +34,128 @@ export interface DependencyInfo { icon: string version: string errorText: string - actionText: string + actionText: string | null action: () => any } -const STATES = [ - PackageState.Installing, - PackageState.Updating, - PackageState.Restoring, -] - @Component({ selector: 'app-show', templateUrl: './app-show.page.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppShowPage { - private readonly pkgId = getPkgId(this.route) + readonly pkgId = getPkgId(this.route) readonly pkgPlus$ = combineLatest([ - this.patch.watch$('package-data', this.pkgId), + this.patch.watch$('packageData'), this.depErrorService.getPkgDepErrors$(this.pkgId), ]).pipe( - tap(([pkg, _]) => { + tap(([allPkgs, _]) => { + const pkg = allPkgs[this.pkgId] // if package disappears, navigate to list page if (!pkg) this.navCtrl.navigateRoot('/services') }), - map(([pkg, depErrors]) => { + map(([allPkgs, depErrors]) => { + const pkg = allPkgs[this.pkgId] + const manifest = getManifest(pkg) return { + allPkgs, pkg, - dependencies: this.getDepInfo(pkg, depErrors), + manifest, + dependencies: this.getDepInfo(pkg, manifest, allPkgs, depErrors), status: renderPkgStatus(pkg, depErrors), } }), ) + isInstalled = isInstalled + constructor( private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly patch: PatchDB, - private readonly modalService: ModalService, private readonly depErrorService: DepErrorService, ) {} - isInstalled({ state }: PackageDataEntry): boolean { - return state === PackageState.Installed - } - - isRunning({ primary }: PackageStatus): boolean { - return primary === PrimaryStatus.Running - } - - isBackingUp({ primary }: PackageStatus): boolean { - return primary === PrimaryStatus.BackingUp - } - - showProgress({ state }: PackageDataEntry): boolean { - return STATES.includes(state) + showProgress( + pkg: PackageDataEntry, + ): pkg is PackageDataEntry { + return isInstalling(pkg) || isUpdating(pkg) || isRestoring(pkg) } private getDepInfo( pkg: PackageDataEntry, + manifest: T.Manifest, + allPkgs: AllPackageData, depErrors: PkgDependencyErrors, ): DependencyInfo[] { - const pkgInstalled = pkg.installed - - if (!pkgInstalled) return [] - - return Object.keys(pkgInstalled['current-dependencies']) - .filter(id => !!pkgInstalled.manifest.dependencies[id]) - .map(id => this.getDepValues(pkgInstalled, id, depErrors)) + return Object.keys(pkg.currentDependencies).map(id => + this.getDepValues(pkg, allPkgs, manifest, id, depErrors), + ) } private getDepValues( - pkgInstalled: InstalledPackageDataEntry, + pkg: PackageDataEntry, + allPkgs: AllPackageData, + manifest: T.Manifest, depId: string, depErrors: PkgDependencyErrors, ): DependencyInfo { const { errorText, fixText, fixAction } = this.getDepErrors( - pkgInstalled, + pkg, + manifest, depId, depErrors, ) - const depInfo = pkgInstalled['dependency-info'][depId] + const { title, icon, versionRange } = getDepDetails(pkg, allPkgs, depId) return { id: depId, - version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range? - title: depInfo?.title || depId, - icon: depInfo?.icon || '', - errorText: errorText - ? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.` - : '', - actionText: fixText || 'View', - action: - fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)), + version: versionRange, + title, + icon, + errorText: errorText ? errorText : '', + actionText: fixText, + action: fixAction, } } private getDepErrors( - pkgInstalled: InstalledPackageDataEntry, + pkg: PackageDataEntry, + manifest: T.Manifest, depId: string, depErrors: PkgDependencyErrors, ) { - const pkgManifest = pkgInstalled.manifest const depError = depErrors[depId] let errorText: string | null = null let fixText: string | null = null - let fixAction: (() => any) | null = null + let fixAction: () => any = () => {} if (depError) { - if (depError.type === DependencyErrorType.NotInstalled) { + if (depError.type === 'notInstalled') { errorText = 'Not installed' fixText = 'Install' - fixAction = () => this.fixDep(pkgManifest, 'install', depId) - } else if (depError.type === DependencyErrorType.IncorrectVersion) { + fixAction = () => this.installDep(pkg, manifest, depId) + } else if (depError.type === 'incorrectVersion') { errorText = 'Incorrect version' fixText = 'Update' - fixAction = () => this.fixDep(pkgManifest, 'update', depId) - } else if (depError.type === DependencyErrorType.ConfigUnsatisfied) { - errorText = 'Config not satisfied' - fixText = 'Auto config' - fixAction = () => this.fixDep(pkgManifest, 'configure', depId) - } else if (depError.type === DependencyErrorType.NotRunning) { + fixAction = () => this.installDep(pkg, manifest, depId) + } else if (depError.type === 'actionRequired') { + errorText = 'Action Required (see below)' + } else if (depError.type === 'notRunning') { errorText = 'Not running' fixText = 'Start' - } else if (depError.type === DependencyErrorType.HealthChecksFailed) { + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) + } else if (depError.type === 'healthChecksFailed') { errorText = 'Required health check not passing' - } else if (depError.type === DependencyErrorType.Transitive) { + fixText = 'View' + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) + } else if (depError.type === 'transitive') { errorText = 'Dependency has a dependency issue' + fixText = 'View' + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) } } @@ -172,30 +166,15 @@ export class AppShowPage { } } - private async fixDep( - pkgManifest: Manifest, - action: 'install' | 'update' | 'configure', - id: string, - ): Promise { - switch (action) { - case 'install': - case 'update': - return this.installDep(pkgManifest, id) - case 'configure': - return this.configureDep(pkgManifest, id) - } - } - private async installDep( - pkgManifest: Manifest, + pkg: PackageDataEntry, + pkgManifest: T.Manifest, depId: string, ): Promise { - const version = pkgManifest.dependencies[depId].version - const dependentInfo: DependentInfo = { id: pkgManifest.id, title: pkgManifest.title, - version, + version: pkg.currentDependencies[depId].versionRange, } const navigationExtras: NavigationExtras = { state: { dependentInfo }, @@ -206,19 +185,4 @@ export class AppShowPage { navigationExtras, ) } - - private async configureDep( - pkgManifest: Manifest, - dependencyId: string, - ): Promise { - const dependentInfo: DependentInfo = { - id: pkgManifest.id, - title: pkgManifest.title, - } - - await this.modalService.presentModalConfig({ - pkgId: dependencyId, - dependentInfo, - }) - } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html new file mode 100644 index 000000000..2ee3d6698 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html @@ -0,0 +1,42 @@ + + + Action Requests + +
+ + + +

{{ request.actionName }}

+

+ {{ request.reason || 'no reason provided' }} | + + {{ request.severity === 'critical' ? 'Required' : 'Requested' }} + +

+
+
+
+
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss new file mode 100644 index 000000000..0120a3060 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss @@ -0,0 +1,39 @@ +ion-icon { + margin-right: 32px; +} + +.highlighted { + color: var(--ion-color-dark); + font-weight: bold; +} + +.severity { + font-variant-caps: all-small-caps; + font-weight: bold; + letter-spacing: 0.2px; + font-size: 16px; +} + +.line { + + &:after { + content: ''; + display: block; + border-left: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + height: 100%; + width: 24px; + position: absolute; + left: -20px; + top: -33px; + } +} + +.indent { + margin-left: 41px +} + +:host ::ng-deep ion-item { + display: table-row; + width: fit-content; +} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts new file mode 100644 index 000000000..37c66aecf --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts @@ -0,0 +1,91 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { ActionService } from 'src/app/services/action.service' +import { DependencyInfo } from 'src/app/pages/apps-routes/app-show/app-show.page' +import { getDepDetails } from 'src/app/util/dep-info' + +@Component({ + selector: 'app-show-action-requests', + templateUrl: './app-show-action-requests.component.html', + styleUrls: ['./app-show-action-requests.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppShowActionRequestsComponent { + @Input() + allPkgs!: Record + + @Input() + pkg!: T.PackageDataEntry + + @Input() + manifest!: T.Manifest + + @Input() + dep?: DependencyInfo + + pkgId!: string + + ngOnInit() { + this.pkgId = this.dep ? this.dep?.id : this.manifest.id + } + + get actionRequests() { + const reqs: { + [key: string]: (T.ActionRequest & { + actionName: string + })[] + } = {} + Object.values(this.pkg.requestedActions) + .filter(r => r.active) + .forEach(r => { + const self = r.request.packageId === this.manifest.id + const toReturn = { + ...r.request, + actionName: self + ? this.pkg.actions[r.request.actionId].name + : this.allPkgs[r.request.packageId]?.actions[r.request.actionId] + .name || 'Unknown Action', + dependency: self + ? null + : getDepDetails(this.pkg, this.allPkgs, r.request.packageId), + } + if (!reqs[r.request.packageId]) { + reqs[r.request.packageId] = [] + } + reqs[r.request.packageId].push(toReturn) + }) + return reqs + } + constructor(private readonly actionService: ActionService) {} + + async handleAction(request: T.ActionRequest, e: Event) { + e.stopPropagation() + const self = request.packageId === this.manifest.id + this.actionService.present({ + pkgInfo: { + id: request.packageId, + title: self + ? this.manifest.title + : getDepDetails(this.pkg, this.allPkgs, request.packageId).title, + mainStatus: self + ? this.pkg.status.main + : this.allPkgs[request.packageId].status.main, + icon: self + ? this.pkg.icon + : getDepDetails(this.pkg, this.allPkgs, request.packageId).icon, + }, + actionInfo: { + id: request.actionId, + metadata: + request.packageId === this.manifest.id + ? this.pkg.actions[request.actionId] + : this.allPkgs[request.packageId].actions[request.actionId], + }, + requestInfo: { + request, + dependentId: + request.packageId === this.manifest.id ? undefined : this.manifest.id, + }, + }) + } +} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html index 81fe8fb84..e524e13b5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html @@ -1,16 +1,16 @@ Additional Info - +

Version

-

{{ manifest.version | displayEmver }}

+

{{ pkg.stateInfo.manifest.version }}

Git Hash

License

-

{{ manifest.license }}

+

{{ pkg.stateInfo.manifest.license }}

Marketing Site

-

{{ manifest['marketing-site'] || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.marketingSite || 'Not provided' }}

@@ -54,52 +54,52 @@

Marketing Site

Source Repository

-

{{ manifest['upstream-repo'] }}

+

{{ pkg.stateInfo.manifest.upstreamRepo }}

Wrapper Repository

-

{{ manifest['wrapper-repo'] }}

+

{{ pkg.stateInfo.manifest.wrapperRepo }}

Support Site

-

{{ manifest['support-site'] || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.supportSite || 'Not provided' }}

Donation Link

-

{{ manifest['donation-url'] || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.donationUrl || 'Not provided' }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts index 501898428..9aa152917 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts @@ -3,7 +3,10 @@ import { ModalController, ToastController } from '@ionic/angular' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' +import { + InstalledState, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-additional', @@ -12,7 +15,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' }) export class AppShowAdditionalComponent { @Input() - pkg!: PackageDataEntry + pkg!: PackageDataEntry constructor( private readonly modalCtrl: ModalController, @@ -35,10 +38,12 @@ export class AppShowAdditionalComponent { } async presentModalLicense() { + const { id } = this.pkg.stateInfo.manifest + const modal = await this.modalCtrl.create({ componentProps: { title: 'License', - content: from(this.api.getStatic(this.pkg['static-files']['license'])), + content: from(this.api.getStaticInstalled(id, 'LICENSE.md')), }, component: MarkdownComponent, }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html index e9f7b97d7..3b96a5ac8 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html @@ -1,27 +1,41 @@ Dependencies - - - - - -

- - {{ dep.title }} -

-

{{ dep.version | displayEmver }}

-

- - {{ dep.errorText || 'satisfied' }} - -

-
+ +
+
+ + + + +

+ + {{ dep.title }} +

+

{{ dep.version }}

+

+ + {{ dep.errorText || 'satisfied' }} + +

+
+
+ +
{{ dep.actionText }} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.scss b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.scss index dd0cbe2b8..80b1dad30 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.scss +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.scss @@ -1,3 +1,26 @@ .icon { padding-right: 4px; } + +img { + position: relative; + z-index: 10; +} + +.container { + display: flex; + flex-direction: column; + margin: 8px; +} + +.dep-details { + display: flex; + align-items: center; + flex-direction: row; + gap: 1.2rem; +} + +ion-label h2 { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.ts index 3a2fee53b..097aa06c2 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.ts @@ -1,5 +1,10 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { DependencyInfo } from '../../app-show.page' +import { T } from '@start9labs/start-sdk' +import { + PackageDataEntry, + StateInfo, +} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-dependencies', @@ -10,4 +15,15 @@ import { DependencyInfo } from '../../app-show.page' export class AppShowDependenciesComponent { @Input() dependencies: DependencyInfo[] = [] + + @Input() + allPkgs!: NonNullable< + T.AllPackageData & Record> + > + + @Input() + pkg!: T.PackageDataEntry & { stateInfo: StateInfo } + + @Input() + manifest!: T.Manifest } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.html new file mode 100644 index 000000000..c056f2977 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.html @@ -0,0 +1,31 @@ +Message +
+ + {{ error.message }} + +
+ +Actions +
+

+ Rebuild Container + is harmless action that and only takes a few seconds to complete. It will + likely resolve this issue. + Uninstall Service + is a dangerous action that will remove the service from StartOS and wipe all + its data. +

+ + Rebuild Container + + + Uninstall Service + +
+ + + Full Stack Trace +
+ {{ error.message }} +
+
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.ts new file mode 100644 index 000000000..ef689f178 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-error/app-show-error.component.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { ToastController } from '@ionic/angular' +import { copyToClipboard } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { StandardActionsService } from 'src/app/services/standard-actions.service' + +@Component({ + selector: 'app-show-error', + templateUrl: 'app-show-error.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppShowErrorComponent { + @Input() + manifest!: T.Manifest + + @Input() + error!: T.MainStatus & { main: 'error' } + + constructor( + private readonly toastCtrl: ToastController, + private readonly standardActionsService: StandardActionsService, + ) {} + + async copy(text: string): Promise { + const success = await copyToClipboard(text) + const message = success + ? 'Copied to clipboard!' + : 'Failed to copy to clipboard.' + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + }) + await toast.present() + } + + async rebuild() { + return this.standardActionsService.rebuild(this.manifest.id) + } + + async tryUninstall() { + return this.standardActionsService.tryUninstall(this.manifest) + } +} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html index 17dcd1be1..efe34b5ec 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html @@ -4,15 +4,12 @@
- - -

- {{ pkg.manifest.title }} + + +

+ {{ manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html index fe854f8fc..b0d65b5fd 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html @@ -1,93 +1,81 @@ - - - Health Checks - - - - - - - - - - -

- {{ pkg.manifest['health-checks'][health.key].name }} -

- -

- {{ result | titlecase }} - ... - - {{ $any(health.value).error }} - - - {{ $any(health.value).message }} - - - : - {{ - pkg.manifest['health-checks'][health.key]['success-message'] - }} - -

-
-
-
- - - - -

- {{ pkg.manifest['health-checks'][health.key].name }} -

-

Awaiting result...

-
-
-
-
- - - - - - + + Health Checks + + + + + + + + + - - +

+ {{ check.value.name }} +

+ +

+ {{ result | titlecase }} + ... + + {{ check.value.message }} + +

+
-
-
+
+ + + + +

+ {{ check.value.name }} +

+

Awaiting result...

+
+
+
+ + + + + + + + + + + +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index 5db1ab1ca..f470bc43a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts @@ -1,9 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { T } from '@start9labs/start-sdk' import { ConnectionService } from 'src/app/services/connection.service' -import { - HealthResult, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-health-checks', @@ -13,20 +10,16 @@ import { }) export class AppShowHealthChecksComponent { @Input() - pkg!: PackageDataEntry + healthChecks!: Record - HealthResult = HealthResult + constructor(readonly connection$: ConnectionService) {} - readonly connected$ = this.connectionService.connected$ - - constructor(private readonly connectionService: ConnectionService) {} - - isLoading(result: HealthResult): boolean { - return result === HealthResult.Starting || result === HealthResult.Loading + isLoading(result: T.NamedHealthCheckResult['result']): boolean { + return result === 'starting' || result === 'loading' } - isReady(result: HealthResult): boolean { - return result !== HealthResult.Failure && result !== HealthResult.Loading + isReady(result: T.NamedHealthCheckResult['result']): boolean { + return result !== 'failure' && result !== 'loading' } asIsOrder() { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html index 3134cee5e..1b1714f64 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html @@ -1,20 +1,20 @@ -

Downloading: {{ progressData.downloadProgress }}%

- - -

Validating: {{ progressData.validateProgress }}%

- - -

Unpacking: {{ progressData.unpackProgress }}%

- + +

+ {{ phase.name }} + + : {{ progress }}% + +

+ +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts index 8ee7b750a..4c0f83433 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts @@ -1,9 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { - InstallProgress, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' -import { ProgressData } from 'src/app/types/progress-data' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-show-progress', @@ -13,26 +9,5 @@ import { ProgressData } from 'src/app/types/progress-data' }) export class AppShowProgressComponent { @Input() - pkg!: PackageDataEntry - - @Input() - progressData!: ProgressData - - get unpackingBuffer(): number { - return this.progressData.validateProgress === 100 && - !this.progressData.unpackProgress - ? 0 - : 1 - } - - get validationBuffer(): number { - return this.progressData.downloadProgress === 100 && - !this.progressData.validateProgress - ? 0 - : 1 - } - - getColor(action: keyof InstallProgress): string { - return this.pkg['install-progress']?.[action] ? 'success' : 'secondary' - } + phases!: T.NamedProgress[] } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index 6a8676ebc..ef8f9a44e 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -4,14 +4,21 @@
- + @@ -36,7 +43,7 @@ - - Configure - - - Launch UI diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index f67a2b2fa..70b969411 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -1,22 +1,25 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { UiLauncherService } from 'src/app/services/ui-launcher.service' +import { AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { PatchDB } from 'patch-db-client' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConnectionService } from 'src/app/services/connection.service' +import { + DataModel, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' import { PackageStatus, PrimaryRendering, - PrimaryStatus, } from 'src/app/services/pkg-status-rendering.service' +import { UiLauncherService } from 'src/app/services/ui-launcher.service' import { - InterfaceDef, - PackageDataEntry, - PackageState, - Status, -} from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' -import { AlertController, LoadingController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ModalService } from 'src/app/services/modal.service' + getAllPackages, + getManifest, + isInstalled, +} from 'src/app/util/get-package-data' import { hasCurrentDeps } from 'src/app/util/has-deps' -import { ConnectionService } from 'src/app/services/connection.service' @Component({ selector: 'app-show-status', @@ -33,65 +36,66 @@ export class AppShowStatusComponent { PR = PrimaryRendering - readonly connected$ = this.connectionService.connected$ + isInstalled = isInstalled constructor( private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly launcherService: UiLauncherService, - private readonly modalService: ModalService, - private readonly connectionService: ConnectionService, + readonly connection$: ConnectionService, + private readonly patch: PatchDB, ) {} - get interfaces(): Record { - return this.pkg.manifest.interfaces || {} + get interfaces(): PackageDataEntry['serviceInterfaces'] { + return this.pkg.serviceInterfaces + } + + get hosts(): PackageDataEntry['hosts'] { + return this.pkg.hosts } - get pkgStatus(): Status | null { - return this.pkg.installed?.status || null + get pkgStatus(): T.MainStatus { + return this.pkg.status } - get isInstalled(): boolean { - return this.pkg.state === PackageState.Installed + get manifest(): T.Manifest { + return getManifest(this.pkg) } get isRunning(): boolean { - return this.status.primary === PrimaryStatus.Running + return this.status.primary === 'running' } get canStop(): boolean { - return [ - PrimaryStatus.Running, - PrimaryStatus.Starting, - PrimaryStatus.Restarting, - ].includes(this.status.primary) + return ['running', 'starting', 'restarting'].includes(this.status.primary) } - get isStopped(): boolean { - return this.status.primary === PrimaryStatus.Stopped + get canStart(): boolean { + return this.status.primary === 'stopped' } - launchUi(): void { - this.launcherService.launch(this.pkg) + get sigtermTimeout(): string | null { + return this.pkgStatus?.main === 'stopping' ? '30s' : null // @TODO Aiden } - async presentModalConfig(): Promise { - return this.modalService.presentModalConfig({ - pkgId: this.id, - }) + launchUi( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { + this.launcherService.launch(interfaces, hosts) } async tryStart(): Promise { if (this.status.dependency === 'warning') { - const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.` + const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.` const proceed = await this.presentAlertStart(depErrMsg) if (!proceed) return } - const alertMsg = this.pkg.manifest.alerts.start + const alertMsg = this.manifest.alerts.start if (alertMsg) { const proceed = await this.presentAlertStart(alertMsg) @@ -103,10 +107,10 @@ export class AppShowStatusComponent { } async tryStop(): Promise { - const { title, alerts } = this.pkg.manifest + const { title, alerts } = this.manifest let message = alerts.stop || '' - if (hasCurrentDeps(this.pkg)) { + if (hasCurrentDeps(this.manifest.id, await getAllPackages(this.patch))) { const depMessage = `Services that depend on ${title} will no longer work properly and may crash` message = message ? `${message}.\n\n${depMessage}` : depMessage } @@ -138,10 +142,10 @@ export class AppShowStatusComponent { } async tryRestart(): Promise { - if (hasCurrentDeps(this.pkg)) { + if (hasCurrentDeps(this.manifest.id, await getAllPackages(this.patch))) { const alert = await this.alertCtrl.create({ header: 'Warning', - message: `Services that depend on ${this.pkg.manifest.title} may temporarily experiences issues`, + message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`, buttons: [ { text: 'Cancel', @@ -164,54 +168,42 @@ export class AppShowStatusComponent { } } - private get id(): string { - return this.pkg.manifest.id - } - private async start(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Starting...`, - }) - await loader.present() + const loader = this.loader.open(`Starting...`).subscribe() try { - await this.embassyApi.startPackage({ id: this.id }) + await this.embassyApi.startPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async stop(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Stopping...', - }) - await loader.present() + const loader = this.loader.open('Stopping...').subscribe() try { - await this.embassyApi.stopPackage({ id: this.id }) + await this.embassyApi.stopPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async restart(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Restarting...`, - }) - await loader.present() + const loader = this.loader.open(`Restarting...`).subscribe() try { - await this.embassyApi.restartPackage({ id: this.id }) + await this.embassyApi.restartPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } + private async presentAlertStart(message: string): Promise { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts index a274aa8c0..1f27b5e46 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts @@ -1,20 +1,20 @@ import { Pipe, PipeTransform } from '@angular/core' -import { HealthResult } from 'src/app/services/patch-db/data-model' +import { T } from '@start9labs/start-sdk' @Pipe({ name: 'healthColor', }) export class HealthColorPipe implements PipeTransform { - transform(val: HealthResult): string { + transform(val: T.NamedHealthCheckResult['result']): string { switch (val) { - case HealthResult.Success: + case 'success': return 'success' - case HealthResult.Failure: + case 'failure': return 'warning' - case HealthResult.Disabled: + case 'disabled': return 'dark' - case HealthResult.Starting: - case HealthResult.Loading: + case 'starting': + case 'loading': return 'primary' } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts deleted file mode 100644 index 1e5397648..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { ProgressData } from 'src/app/types/progress-data' -import { packageLoadingProgress } from 'src/app/util/package-loading-progress' - -@Pipe({ - name: 'progressData', -}) -export class ProgressDataPipe implements PipeTransform { - transform(pkg: PackageDataEntry): ProgressData | null { - return packageLoadingProgress(pkg['install-progress']) - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index eeb00b435..b2101ce29 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -4,9 +4,9 @@ import { ModalController, NavController } from '@ionic/angular' import { MarkdownComponent } from '@start9labs/shared' import { DataModel, + InstalledState, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { ModalService } from 'src/app/services/modal.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { from, map, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' @@ -28,50 +28,31 @@ export class ToButtonsPipe implements PipeTransform { private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, - private readonly modalService: ModalService, private readonly apiService: ApiService, + private readonly api: ApiService, private readonly patch: PatchDB, ) {} - transform(pkg: PackageDataEntry): Button[] { - const pkgTitle = pkg.manifest.title + transform(pkg: PackageDataEntry): Button[] { + const manifest = pkg.stateInfo.manifest return [ // instructions { action: () => this.presentModalInstructions(pkg), title: 'Instructions', - description: `Understand how to use ${pkgTitle}`, + description: `Understand how to use ${manifest.title}`, icon: 'list-outline', highlighted$: this.patch - .watch$('ui', 'ack-instructions', pkg.manifest.id) + .watch$('ui', 'ackInstructions', manifest.id) .pipe(map(seen => !seen)), }, - // config - { - action: async () => - this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }), - title: 'Config', - description: `Customize ${pkgTitle}`, - icon: 'options-outline', - }, - // properties - { - action: () => - this.navCtrl.navigateForward(['properties'], { - relativeTo: this.route, - }), - title: 'Properties', - description: - 'Runtime information, credentials, and other values of interest', - icon: 'briefcase-outline', - }, // actions { action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), title: 'Actions', - description: `Uninstall and other commands specific to ${pkgTitle}`, + description: `All actions for ${manifest.title}`, icon: 'flash-outline', }, // interfaces @@ -97,16 +78,21 @@ export class ToButtonsPipe implements PipeTransform { ] } - private async presentModalInstructions(pkg: PackageDataEntry) { + private async presentModalInstructions( + pkg: PackageDataEntry, + ) { this.apiService - .setDbValue(['ack-instructions', pkg.manifest.id], true) + .setDbValue(['ackInstructions', pkg.stateInfo.manifest.id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) const modal = await this.modalCtrl.create({ componentProps: { title: 'Instructions', content: from( - this.apiService.getStatic(pkg['static-files']['instructions']), + this.api.getStaticInstalled( + pkg.stateInfo.manifest.id, + 'instructions.md', + ), ), }, component: MarkdownComponent, @@ -115,17 +101,22 @@ export class ToButtonsPipe implements PipeTransform { await modal.present() } - private viewInMarketplaceButton(pkg: PackageDataEntry): Button { - const url = pkg.installed?.['marketplace-url'] + private viewInMarketplaceButton( + pkg: PackageDataEntry, + ): Button { + const url = pkg.registry const queryParams = url ? { url } : {} let button: Button = { title: 'Marketplace Listing', icon: 'storefront-outline', action: () => - this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], { - queryParams, - }), + this.navCtrl.navigateForward( + [`marketplace/${pkg.stateInfo.manifest.id}`], + { + queryParams, + }, + ), disabled: false, description: 'View service in the marketplace', } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts index 8ba9bd4f3..12d731010 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts @@ -1,14 +1,10 @@ import { Pipe, PipeTransform } from '@angular/core' -import { - DataModel, - HealthCheckResult, - PackageDataEntry, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { isEmptyObject } from '@start9labs/shared' import { map, startWith } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { Observable } from 'rxjs' +import { T } from '@start9labs/start-sdk' @Pipe({ name: 'toHealthChecks', @@ -17,27 +13,15 @@ export class ToHealthChecksPipe implements PipeTransform { constructor(private readonly patch: PatchDB) {} transform( - pkg: PackageDataEntry, - ): Observable> | null { - const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce( - (obj, key) => ({ ...obj, [key]: null }), - {}, + manifest: T.Manifest, + ): Observable | null> { + return this.patch.watch$('packageData', manifest.id, 'status').pipe( + map(status => { + return status.main === 'running' && !isEmptyObject(status.health) + ? status.health + : null + }), + startWith(null), ) - - const healthChecks$ = this.patch - .watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main') - .pipe( - map(main => { - // Question: is this ok or do we have to use Object.keys - // to maintain order and the keys initially present in pkg? - return main.status === PackageMainStatus.Running && - !isEmptyObject(main.health) - ? main.health - : healthChecks - }), - startWith(healthChecks), - ) - - return isEmptyObject(healthChecks) ? null : healthChecks$ } } diff --git a/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts b/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts index 9dfbddcad..06f4b45fe 100644 --- a/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -36,20 +36,6 @@ const routes: Routes = [ loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), }, - { - path: ':pkgId/metrics', - loadChildren: () => - import('./app-metrics/app-metrics.module').then( - m => m.AppMetricsPageModule, - ), - }, - { - path: ':pkgId/properties', - loadChildren: () => - import('./app-properties/app-properties.module').then( - m => m.AppPropertiesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts b/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts deleted file mode 100644 index 649ab7dfc..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevConfigPage } from './dev-config.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevConfigPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevConfigPage], -}) -export class DevConfigPageModule {} diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html b/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html deleted file mode 100644 index 334236d1e..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - Config - - - - Preview - - - - - - - diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss b/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts deleted file mode 100644 index 6dc4c7e13..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' -import { debounce, ErrorToastService } from '@start9labs/shared' -import * as yaml from 'js-yaml' -import { filter, take } from 'rxjs/operators' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { GenericFormPage } from '../../../modals/generic-form/generic-form.page' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'dev-config', - templateUrl: 'dev-config.page.html', - styleUrls: ['dev-config.page.scss'], -}) -export class DevConfigPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'yaml' } - code: string = '' - saving: boolean = false - - constructor( - private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, - private readonly modalCtrl: ModalController, - private readonly patch: PatchDB, - private readonly api: ApiService, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId, 'config') - .pipe(filter(Boolean), take(1)) - .subscribe(config => { - this.code = config - }) - } - - async preview() { - let doc: any - try { - doc = yaml.load(this.code) - } catch (e: any) { - this.errToast.present(e) - } - - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Config Sample', - spec: JSON.parse(JSON.stringify(doc, null, 2)), - buttons: [ - { - text: 'OK', - handler: () => { - return - }, - isSubmit: true, - }, - ], - }, - }) - await modal.present() - } - - @debounce(1000) - async save() { - this.saving = true - try { - await this.api.setDbValue( - ['dev', this.projectId, 'config'], - this.code, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - } - } -} diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts b/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts deleted file mode 100644 index ce15130e5..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevInstructionsPage } from './dev-instructions.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevInstructionsPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevInstructionsPage], -}) -export class DevInstructionsPageModule {} diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html b/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html deleted file mode 100644 index 8775ec175..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - Instructions - - - - Preview - - - - - - - diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.scss b/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts deleted file mode 100644 index bf3fa0b56..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' -import { filter, take } from 'rxjs/operators' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - debounce, - ErrorToastService, - MarkdownComponent, -} from '@start9labs/shared' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'dev-instructions', - templateUrl: 'dev-instructions.page.html', - styleUrls: ['dev-instructions.page.scss'], -}) -export class DevInstructionsPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'markdown' } - code = '' - saving = false - - constructor( - private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, - private readonly modalCtrl: ModalController, - private readonly patch: PatchDB, - private readonly api: ApiService, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId, 'instructions') - .pipe(filter(Boolean), take(1)) - .subscribe(config => { - this.code = config - }) - } - - async preview() { - const modal = await this.modalCtrl.create({ - componentProps: { - title: 'Instructions Sample', - content: this.code, - }, - component: MarkdownComponent, - }) - - await modal.present() - } - - @debounce(1000) - async save() { - this.saving = true - try { - await this.api.setDbValue( - ['dev', this.projectId, 'instructions'], - this.code, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - } - } -} diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts b/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts deleted file mode 100644 index 49d738fe8..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevManifestPage } from './dev-manifest.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevManifestPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevManifestPage], -}) -export class DevManifestPageModule {} diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html b/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html deleted file mode 100644 index 595adb6c3..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Manifest - - - - - - diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss b/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts b/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts deleted file mode 100644 index f039abbd8..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import * as yaml from 'js-yaml' -import { take } from 'rxjs/operators' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'dev-manifest', - templateUrl: 'dev-manifest.page.html', - styleUrls: ['dev-manifest.page.scss'], -}) -export class DevManifestPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true } - manifest: string = '' - - constructor( - private readonly route: ActivatedRoute, - private readonly patch: PatchDB, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId) - .pipe(take(1)) - .subscribe(devData => { - this.manifest = yaml.dump(devData['basic-info']) - }) - } -} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts b/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts deleted file mode 100644 index ea0c78423..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DeveloperListPage } from './developer-list.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' - -const routes: Routes = [ - { - path: '', - component: DeveloperListPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - ], - declarations: [DeveloperListPage], -}) -export class DeveloperListPageModule {} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html deleted file mode 100644 index e00330448..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Developer Tools - - - - - - - - Projects - - - - - Create project - - - - -

{{ entry.value.name }}

- - - -
-
diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.scss b/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts deleted file mode 100644 index 96aa6126a..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Component } from '@angular/core' -import { - ActionSheetButton, - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import * as yaml from 'js-yaml' -import { v4 } from 'uuid' -import { DataModel, DevData } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { takeUntil } from 'rxjs/operators' - -@Component({ - selector: 'developer-list', - templateUrl: 'developer-list.page.html', - styleUrls: ['developer-list.page.scss'], - providers: [TuiDestroyService], -}) -export class DeveloperListPage { - devData: DevData = {} - - constructor( - private readonly modalCtrl: ModalController, - private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, - private readonly destroy$: TuiDestroyService, - private readonly patch: PatchDB, - private readonly actionCtrl: ActionSheetController, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev') - .pipe(takeUntil(this.destroy$)) - .subscribe(dd => { - this.devData = dd - }) - } - - async openCreateProjectModal() { - const projNumber = Object.keys(this.devData).length + 1 - const options: GenericInputOptions = { - title: 'Add new project', - message: 'Create a new dev project.', - label: 'New project', - useMask: false, - placeholder: `Project ${projNumber}`, - nullable: true, - initialValue: `Project ${projNumber}`, - buttonText: 'Save', - submitFn: (value: string) => this.createProject(value), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - async presentAction(id: string, event: Event) { - event.stopPropagation() - event.preventDefault() - const buttons: ActionSheetButton[] = [ - { - text: 'Edit Name', - icon: 'pencil', - handler: () => { - this.openEditNameModal(id) - }, - }, - { - text: 'Delete', - icon: 'trash', - role: 'destructive', - handler: () => { - this.presentAlertDelete(id) - }, - }, - ] - - const action = await this.actionCtrl.create({ - header: this.devData[id].name, - subHeader: 'Manage project', - mode: 'ios', - buttons, - }) - - await action.present() - } - - async openEditNameModal(id: string) { - const curName = this.devData[id].name - const options: GenericInputOptions = { - title: 'Edit Name', - message: 'Edit the name of your project.', - label: 'Name', - useMask: false, - placeholder: curName, - nullable: true, - initialValue: curName, - buttonText: 'Save', - submitFn: (value: string) => this.editName(id, value), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - async createProject(name: string) { - // fail silently if duplicate project name - if ( - Object.values(this.devData) - .map(v => v.name) - .includes(name) - ) - return - - const loader = await this.loadingCtrl.create({ - message: 'Creating Project...', - }) - await loader.present() - - try { - const id = v4() - const config = yaml - .dump(SAMPLE_CONFIG) - .replace(/warning:/g, '# Optional\n warning:') - - const def = { name, config, instructions: SAMPLE_INSTUCTIONS } - await this.api.setDbValue<{ - name: string - config: string - instructions: string - }>(['dev', id], def) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - async presentAlertDelete(id: string) { - const alert = await this.alertCtrl.create({ - header: 'Caution', - message: `Are you sure you want to delete this project?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.delete(id) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async editName(id: string, newName: string) { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.api.setDbValue(['dev', id, 'name'], newName) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - async delete(id: string) { - const loader = await this.loadingCtrl.create({ - message: 'Removing Project...', - }) - await loader.present() - - try { - const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData)) - delete devDataToSave[id] - await this.api.setDbValue(['dev'], devDataToSave) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } -} - -const SAMPLE_INSTUCTIONS = `# Create Instructions using Markdown! :)` - -const SAMPLE_CONFIG: ConfigSpec = { - 'sample-string': { - type: 'string', - name: 'Example String Input', - nullable: false, - masked: false, - copyable: false, - // optional - description: 'Example description for required string input.', - placeholder: 'Enter string value', - pattern: '^[a-zA-Z0-9! _]+$', - 'pattern-description': 'Must be alphanumeric (may contain underscore).', - }, - 'sample-number': { - type: 'number', - name: 'Example Number Input', - nullable: false, - range: '[5,1000000]', - integral: true, - // optional - warning: 'Example warning to display when changing this number value.', - units: 'ms', - description: 'Example description for optional number input.', - placeholder: 'Enter number value', - }, - 'sample-boolean': { - type: 'boolean', - name: 'Example Boolean Toggle', - // optional - description: 'Example description for boolean toggle', - default: true, - }, - 'sample-enum': { - type: 'enum', - name: 'Example Enum Select', - values: ['red', 'blue', 'green'], - 'value-names': { - red: 'Red', - blue: 'Blue', - green: 'Green', - }, - // optional - warning: 'Example warning to display when changing this enum value.', - description: 'Example description for enum select', - default: 'red', - }, -} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts b/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts deleted file mode 100644 index d33ecf47f..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DeveloperMenuPage } from './developer-menu.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' -import { FormsModule } from '@angular/forms' -import { SharedPipesModule } from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: DeveloperMenuPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - GenericFormPageModule, - FormsModule, - MonacoEditorModule, - SharedPipesModule, - ], - declarations: [DeveloperMenuPage], -}) -export class DeveloperMenuPageModule {} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html b/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html deleted file mode 100644 index ee10e7fc6..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - {{ (projectData$ | async)?.name || '' }} - - View Manifest - - - - - - - - -

Basic Info

-

Complete basic info for your package

-
- - -
- - - -

Instructions Generator

-

Create instructions and see how they will appear to the end user

-
-
- - - -

Config Generator

-

Edit the config with YAML and see it in real time

-
-
-
diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss b/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts deleted file mode 100644 index 3ae2b394c..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { LoadingController, ModalController } from '@ionic/angular' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { BasicInfo, getBasicInfoSpec } from './form-info' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel, DevProjectData } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'developer-menu', - templateUrl: 'developer-menu.page.html', - styleUrls: ['developer-menu.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeveloperMenuPage { - readonly projectId = getProjectId(this.route) - readonly projectData$ = this.patch.watch$('ui', 'dev', this.projectId) - - constructor( - private readonly route: ActivatedRoute, - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly errToast: ErrorToastService, - private readonly patch: PatchDB, - ) {} - - async openBasicInfoModal(data: DevProjectData) { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Basic Info', - spec: getBasicInfoSpec(data), - buttons: [ - { - text: 'Save', - handler: (basicInfo: BasicInfo) => { - this.saveBasicInfo(basicInfo) - }, - isSubmit: true, - }, - ], - }, - }) - await modal.present() - } - - async saveBasicInfo(basicInfo: BasicInfo) { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.api.setDbValue( - ['dev', this.projectId, 'basic-info'], - basicInfo, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } -} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts b/web/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts deleted file mode 100644 index b8a848f12..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { DevProjectData } from 'src/app/services/patch-db/data-model' - -export type BasicInfo = { - id: string - title: string - 'service-version-number': string - 'release-notes': string - license: string - 'wrapper-repo': string - 'upstream-repo'?: string - 'support-site'?: string - 'marketing-site'?: string - description: { - short: string - long: string - } -} - -export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec { - const basicInfo = devData['basic-info'] - return { - id: { - type: 'string', - name: 'ID', - description: 'The package identifier used by the OS', - placeholder: 'e.g. bitcoind', - nullable: false, - masked: false, - copyable: false, - pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', - 'pattern-description': 'Must be kebab case', - default: basicInfo?.id, - }, - title: { - type: 'string', - name: 'Service Name', - description: 'A human readable service title', - placeholder: 'e.g. Bitcoin Core', - nullable: false, - masked: false, - copyable: false, - default: basicInfo ? basicInfo.title : devData.name, - }, - 'service-version-number': { - type: 'string', - name: 'Service Version', - description: - 'Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service', - placeholder: 'e.g. 0.1.2.3', - nullable: false, - masked: false, - copyable: false, - pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$', - 'pattern-description': 'Must be valid Emver version', - default: basicInfo?.['service-version-number'], - }, - description: { - type: 'object', - name: 'Marketplace Descriptions', - spec: { - short: { - type: 'string', - name: 'Short Description', - description: - 'This is the first description visible to the user in the marketplace', - nullable: false, - masked: false, - copyable: false, - textarea: true, - default: basicInfo?.description?.short, - pattern: '^.{1,320}$', - 'pattern-description': 'Must be shorter than 320 characters', - }, - long: { - type: 'string', - name: 'Long Description', - description: `This description will display with additional details in the service's individual marketplace page`, - nullable: false, - masked: false, - copyable: false, - textarea: true, - default: basicInfo?.description?.long, - pattern: '^.{1,5000}$', - 'pattern-description': 'Must be shorter than 5000 characters', - }, - }, - }, - 'release-notes': { - type: 'string', - name: 'Release Notes', - description: - 'Markdown supported release notes for this version of this service.', - placeholder: 'e.g. Markdown _release notes_ for **Bitcoin Core**', - nullable: false, - masked: false, - copyable: false, - textarea: true, - default: basicInfo?.['release-notes'], - }, - license: { - type: 'enum', - name: 'License', - values: [ - 'gnu-agpl-v3', - 'gnu-gpl-v3', - 'gnu-lgpl-v3', - 'mozilla-public-license-2.0', - 'apache-license-2.0', - 'mit', - 'boost-software-license-1.0', - 'the-unlicense', - 'custom', - ], - 'value-names': { - 'gnu-agpl-v3': 'GNU AGPLv3', - 'gnu-gpl-v3': 'GNU GPLv3', - 'gnu-lgpl-v3': 'GNU LGPLv3', - 'mozilla-public-license-2.0': 'Mozilla Public License 2.0', - 'apache-license-2.0': 'Apache License 2.0', - mit: 'mit', - 'boost-software-license-1.0': 'Boost Software License 1.0', - 'the-unlicense': 'The Unlicense', - custom: 'Custom', - }, - description: 'Example description for enum select', - default: 'mit', - }, - 'wrapper-repo': { - type: 'string', - name: 'Wrapper Repo', - description: - 'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks', - placeholder: 'e.g. www.github.com/example', - nullable: false, - masked: false, - copyable: false, - default: basicInfo?.['wrapper-repo'], - }, - 'upstream-repo': { - type: 'string', - name: 'Upstream Repo', - description: 'The original project repository URL', - placeholder: 'e.g. www.github.com/example', - nullable: true, - masked: false, - copyable: false, - default: basicInfo?.['upstream-repo'], - }, - 'support-site': { - type: 'string', - name: 'Support Site', - description: 'URL to the support site / channel for the project', - placeholder: 'e.g. start9.com/support', - nullable: true, - masked: false, - copyable: false, - default: basicInfo?.['support-site'], - }, - 'marketing-site': { - type: 'string', - name: 'Marketing Site', - description: 'URL to the marketing site / channel for the project', - placeholder: 'e.g. start9.com', - nullable: true, - masked: false, - copyable: false, - default: basicInfo?.['marketing-site'], - }, - } -} diff --git a/web/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts b/web/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts deleted file mode 100644 index 03720d87d..000000000 --- a/web/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { NgModule } from '@angular/core' -import { Routes, RouterModule } from '@angular/router' - -const routes: Routes = [ - { - path: '', - redirectTo: 'projects', - pathMatch: 'full', - }, - { - path: 'projects', - loadChildren: () => - import('./developer-list/developer-list.module').then( - m => m.DeveloperListPageModule, - ), - }, - { - path: 'projects/:projectId', - loadChildren: () => - import('./developer-menu/developer-menu.module').then( - m => m.DeveloperMenuPageModule, - ), - }, - { - path: 'projects/:projectId/config', - loadChildren: () => - import('./dev-config/dev-config.module').then(m => m.DevConfigPageModule), - }, - { - path: 'projects/:projectId/instructions', - loadChildren: () => - import('./dev-instructions/dev-instructions.module').then( - m => m.DevInstructionsPageModule, - ), - }, - { - path: 'projects/:projectId/manifest', - loadChildren: () => - import('./dev-manifest/dev-manifest.module').then( - m => m.DevManifestPageModule, - ), - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class DeveloperRoutingModule {} diff --git a/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts new file mode 100644 index 000000000..4409288c1 --- /dev/null +++ b/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' + +const ROUTES: Routes = [ + { + path: '', + loadChildren: () => + import('./home/home.module').then(m => m.HomePageModule), + }, + { + path: 'logs', + loadChildren: () => + import('./logs/logs.module').then(m => m.LogsPageModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)], + exports: [RouterModule], +}) +export class DiagnosticModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts similarity index 55% rename from web/projects/diagnostic-ui/src/app/pages/home/home.module.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts index 1664b7c72..9565220ae 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.module.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts @@ -1,12 +1,18 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' import { HomePage } from './home.page' -import { HomePageRoutingModule } from './home-routing.module' + +const routes: Routes = [ + { + path: '', + component: HomePage, + }, +] @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, HomePageRoutingModule], + imports: [CommonModule, IonicModule, RouterModule.forChild(routes)], declarations: [HomePage], }) export class HomePageModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.html b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html similarity index 85% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.html rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html index 9cba08258..7605d2a8e 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.page.html +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html @@ -1,12 +1,10 @@
-

- StartOS - Diagnostic Mode -

+
+

StartOS - Diagnostic Mode

+

StartOS version: {{ config.version }}

+

-
- - System Rebuild - -
-
Repair Drive diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.scss b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.scss similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.scss rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.scss diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts similarity index 66% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts index bbda6939f..8912359a3 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts @@ -1,9 +1,11 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' +import { AlertController } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConfigService } from 'src/app/services/config.service' @Component({ - selector: 'app-home', + selector: 'diagnostic-home', templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], }) @@ -18,14 +20,15 @@ export class HomePage { restarted = false constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, private readonly alertCtrl: AlertController, + readonly config: ConfigService, ) {} async ngOnInit() { try { - const error = await this.api.getError() + const error = await this.api.diagnosticGetError() // incorrect drive if (error.code === 15) { this.error = { @@ -86,64 +89,32 @@ export class HomePage { } async restart(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { - await this.api.restart() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } async forgetDrive(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { - await this.api.forgetDrive() - await this.api.restart() + await this.api.diagnosticForgetDrive() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } - async presentAlertSystemRebuild() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - '

This action will tear down all service containers and rebuild them from scratch. No data will be deleted.

A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.

It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.

', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Rebuild', - handler: () => { - try { - this.systemRebuild() - } catch (e) { - console.error(e) - } - }, - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() - } - async presentAlertRepairDisk() { const alert = await this.alertCtrl.create({ header: 'Warning', @@ -174,37 +145,17 @@ export class HomePage { window.location.reload() } - private async systemRebuild(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() - - try { - await this.api.systemRebuild() - await this.api.restart() - this.restarted = true - } catch (e) { - console.error(e) - } finally { - loader.dismiss() - } - } - private async repairDisk(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { - await this.api.repairDisk() - await this.api.restart() + await this.api.diagnosticRepairDisk() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.module.ts similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.module.ts diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.html similarity index 84% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.html index 6abfaa929..970f9083f 100644 --- a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html +++ b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.html @@ -4,6 +4,16 @@ Logs + + Download + + diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.scss b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.scss similarity index 100% rename from web/projects/ui/src/app/modals/action-success/action-success.page.scss rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.scss diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts similarity index 59% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts index 317cd1ea3..d8e6b2253 100644 --- a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts @@ -1,7 +1,13 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' -import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' +import { + DownloadHTMLService, + ErrorService, + LoadingService, + Log, + toLocalIsoString, +} from '@start9labs/shared' +import { ApiService } from 'src/app/services/api/embassy-api.service' var Convert = require('ansi-to-html') var convert = new Convert({ @@ -18,12 +24,14 @@ export class LogsPage { loading = true needInfinite = true startCursor?: string - limit = 200 + limit = 400 isOnBottom = true constructor( private readonly api: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, + private readonly downloadHtml: DownloadHTMLService, ) {} async ngOnInit() { @@ -47,9 +55,33 @@ export class LogsPage { e.target.complete() } + async download() { + const loader = this.loader.open('Processing 10,000 logs...').subscribe() + + try { + const { entries } = await this.api.diagnosticGetLogs({ + before: true, + limit: 10000, + }) + + const styles = { + 'background-color': '#222428', + color: '#e0e0e0', + 'font-family': 'monospace', + } + const html = this.convertToAnsi(entries) + + this.downloadHtml.download('diagnostic-logs.html', html, styles) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + private async getLogs() { try { - const { 'start-cursor': startCursor, entries } = await this.api.getLogs({ + const { startCursor, entries } = await this.api.diagnosticGetLogs({ cursor: this.startCursor, before: !!this.startCursor, limit: this.limit, @@ -89,7 +121,18 @@ export class LogsPage { this.needInfinite = false } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } } + + private convertToAnsi(entries: Log[]) { + return entries + .map( + entry => + `${toLocalIsoString( + new Date(entry.timestamp), + )}  ${convert.toHtml(entry.message)}`, + ) + .join('
') + } } diff --git a/web/projects/ui/src/app/pages/init/init.module.ts b/web/projects/ui/src/app/pages/init/init.module.ts new file mode 100644 index 000000000..07dd71185 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { TuiProgressModule } from '@taiga-ui/kit' +import { LogsModule } from 'src/app/pages/init/logs/logs.module' +import { InitPage } from './init.page' + +const routes: Routes = [ + { + path: '', + component: InitPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + LogsModule, + TuiProgressModule, + RouterModule.forChild(routes), + ], + declarations: [InitPage], +}) +export class InitPageModule {} diff --git a/web/projects/ui/src/app/pages/init/init.page.html b/web/projects/ui/src/app/pages/init/init.page.html new file mode 100644 index 000000000..5cd21bb07 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.html @@ -0,0 +1,18 @@ +
+

+ Initializing StartOS +

+
+ Progress: {{ (progress.total * 100).toFixed(0) }}% +
+ + +

+
+ diff --git a/web/projects/ui/src/app/pages/init/init.page.scss b/web/projects/ui/src/app/pages/init/init.page.scss new file mode 100644 index 000000000..9fbf7098a --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.scss @@ -0,0 +1,23 @@ +section { + border-radius: 0.25rem; + padding: 1rem; + margin: 1.5rem; + text-align: center; + /* TODO: Theme */ + background: #e0e0e0; + color: #333; + --tui-clear-inverse: rgba(0, 0, 0, 0.1); +} + +logs-window { + display: flex; + flex-direction: column; + height: 18rem; + padding: 1rem; + margin: 0 1.5rem auto; + text-align: left; + overflow: hidden; + border-radius: 2rem; + /* TODO: Theme */ + background: #181818; +} diff --git a/web/projects/ui/src/app/pages/init/init.page.ts b/web/projects/ui/src/app/pages/init/init.page.ts new file mode 100644 index 000000000..318881223 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.ts @@ -0,0 +1,11 @@ +import { Component, inject } from '@angular/core' +import { InitService } from 'src/app/pages/init/init.service' + +@Component({ + selector: 'init-page', + templateUrl: 'init.page.html', + styleUrls: ['init.page.scss'], +}) +export class InitPage { + readonly progress$ = inject(InitService) +} diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts new file mode 100644 index 000000000..9fbcc933d --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -0,0 +1,92 @@ +import { inject, Injectable } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { + catchError, + defer, + from, + map, + Observable, + startWith, + switchMap, + tap, +} from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { StateService } from 'src/app/services/state.service' + +interface MappedProgress { + readonly total: number | null + readonly message: string +} + +@Injectable({ providedIn: 'root' }) +export class InitService extends Observable { + private readonly state = inject(StateService) + private readonly api = inject(ApiService) + private readonly progress$ = defer(() => + from(this.api.initGetProgress()), + ).pipe( + switchMap(({ guid, progress }) => + this.api + .openWebsocket$(guid, { + closeObserver: { + next: () => { + this.state.syncState() + }, + }, + }) + .pipe(startWith(progress)), + ), + map(({ phases, overall }) => ({ + total: getOverallDecimal(overall), + message: phases + .filter( + ( + p, + ): p is { + name: string + progress: { + done: number + total: number | null + } + } => p.progress !== true && p.progress !== null, + ) + .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .join(', '), + })), + tap(({ total }) => { + if (total === 1) { + this.state.syncState() + } + }), + catchError((e, caught$) => { + console.error(e) + this.state.syncState() + return caught$ + }), + ) + + constructor() { + super(subscriber => this.progress$.subscribe(subscriber)) + } +} + +function getOverallDecimal(progress: T.Progress): number { + if (progress === true) { + return 1 + } else if (!progress || !progress.total) { + return 0 + } else { + return progress.total && progress.done / progress.total + } +} + +function getPhaseBytes( + progress: + | false + | { + done: number + total: number | null + }, +): string { + return progress === false ? '' : `: (${progress.done}/${progress.total})` +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.component.ts b/web/projects/ui/src/app/pages/init/logs/logs.component.ts new file mode 100644 index 000000000..de05d33bd --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.component.ts @@ -0,0 +1,33 @@ +import { Component, ElementRef, inject } from '@angular/core' +import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer' +import { LogsService } from 'src/app/pages/init/logs/logs.service' + +@Component({ + selector: 'logs-window', + templateUrl: 'logs.template.html', + styles: [ + ` + pre { + margin: 0; + } + `, + ], + providers: [ + { + provide: INTERSECTION_ROOT, + useExisting: ElementRef, + }, + ], +}) +export class LogsComponent { + readonly logs$ = inject(LogsService) + scroll = true + + scrollTo(bottom: HTMLElement) { + if (this.scroll) bottom.scrollIntoView() + } + + onBottom(entries: readonly IntersectionObserverEntry[]) { + this.scroll = entries[entries.length - 1].isIntersecting + } +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.module.ts b/web/projects/ui/src/app/pages/init/logs/logs.module.ts new file mode 100644 index 000000000..ee4a1bc1d --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { IntersectionObserverModule } from '@ng-web-apis/intersection-observer' +import { MutationObserverModule } from '@ng-web-apis/mutation-observer' +import { TuiScrollbarModule } from '@taiga-ui/core' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' +import { LogsComponent } from './logs.component' + +@NgModule({ + imports: [ + CommonModule, + MutationObserverModule, + IntersectionObserverModule, + NgDompurifyModule, + TuiScrollbarModule, + ], + declarations: [LogsComponent], + exports: [LogsComponent], +}) +export class LogsModule {} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.service.ts b/web/projects/ui/src/app/pages/init/logs/logs.service.ts new file mode 100644 index 000000000..7ff3cecd1 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -0,0 +1,51 @@ +import { inject, Injectable } from '@angular/core' +import { Log, toLocalIsoString } from '@start9labs/shared' +import { + bufferTime, + defer, + filter, + map, + Observable, + scan, + switchMap, +} from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +var Convert = require('ansi-to-html') +var convert = new Convert({ + newline: true, + bg: 'transparent', + colors: { + 4: 'Cyan', + }, + escapeXML: true, +}) + +function convertAnsi(entries: readonly any[]): string { + return entries + .map( + ({ timestamp, message }) => + `${toLocalIsoString( + new Date(timestamp), + )}  ${convert.toHtml(message)}`, + ) + .join('
') +} + +@Injectable({ providedIn: 'root' }) +export class LogsService extends Observable { + private readonly api = inject(ApiService) + private readonly log$ = defer(() => + this.api.initFollowLogs({ boot: 0 }), + ).pipe( + switchMap(({ guid }) => this.api.openWebsocket$(guid)), + bufferTime(500), + filter(logs => !!logs.length), + map(convertAnsi), + scan((logs: readonly string[], log) => [...logs, log], []), + ) + + constructor() { + super(subscriber => this.log$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.template.html b/web/projects/ui/src/app/pages/init/logs/logs.template.html new file mode 100644 index 000000000..24ea6d0c1 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.template.html @@ -0,0 +1,9 @@ + +

+  
+
diff --git a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html index 49f65cc14..8f1582db1 100644 --- a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html +++ b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html @@ -97,7 +97,4 @@

Root CA Trusted!

- + diff --git a/web/projects/ui/src/app/pages/login/login.page.html b/web/projects/ui/src/app/pages/login/login.page.html index 99f6abbe8..7e27a16c3 100644 --- a/web/projects/ui/src/app/pages/login/login.page.html +++ b/web/projects/ui/src/app/pages/login/login.page.html @@ -6,30 +6,6 @@ - - diff --git a/web/projects/ui/src/app/pages/login/login.page.ts b/web/projects/ui/src/app/pages/login/login.page.ts index 15f7d588e..29d4321f8 100644 --- a/web/projects/ui/src/app/pages/login/login.page.ts +++ b/web/projects/ui/src/app/pages/login/login.page.ts @@ -1,11 +1,11 @@ +import { DOCUMENT } from '@angular/common' import { Component, Inject } from '@angular/core' -import { getPlatforms, LoadingController } from '@ionic/angular' +import { Router } from '@angular/router' +import { getPlatforms } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' -import { Router } from '@angular/router' import { ConfigService } from 'src/app/services/config.service' -import { DOCUMENT } from '@angular/common' -import { WINDOW } from '@ng-web-apis/common' @Component({ selector: 'login', @@ -20,25 +20,16 @@ export class LoginPage { constructor( private readonly router: Router, private readonly authService: AuthService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, public readonly config: ConfigService, @Inject(DOCUMENT) public readonly document: Document, - @Inject(WINDOW) private readonly windowRef: Window, ) {} - launchHttps() { - const host = this.config.getHost() - this.windowRef.open(`https://${host}`, '_self') - } - async submit() { this.error = '' - const loader = await this.loadingCtrl.create({ - message: 'Logging in...', - }) - await loader.present() + const loader = this.loader.open('Logging in...').subscribe() try { document.cookie = '' @@ -49,6 +40,7 @@ export class LoginPage { await this.api.login({ password: this.password, metadata: { platforms: getPlatforms() }, + ephemeral: window.location.host === 'localhost', }) this.password = '' @@ -58,7 +50,7 @@ export class LoginPage { // code 7 is for incorrect password this.error = e.code === 7 ? 'Invalid Password' : e.message } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index ea1952578..b86d5df12 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { SharedPipesModule, - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, } from '@start9labs/shared' import { @@ -18,7 +18,6 @@ import { import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceListPage } from './marketplace-list.page' -import { MarketplaceSettingsPageModule } from 'src/app/modals/marketplace-settings/marketplace-settings.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' const routes: Routes = [ @@ -35,7 +34,7 @@ const routes: Routes = [ FormsModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, FilterPackagesPipeModule, MarketplaceStatusModule, BadgeMenuComponentModule, @@ -43,7 +42,6 @@ const routes: Routes = [ CategoriesModule, SearchModule, SkeletonModule, - MarketplaceSettingsPageModule, StoreIconComponentModule, ResponsiveColModule, ], diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index 734cb8910..32a6120ec 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -62,9 +62,10 @@

{{ details.name }}

>
diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 089b66139..c08f00703 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,10 +1,11 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { T } from '@start9labs/start-sdk' +import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' -import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page' +import { MARKETPLACE_REGISTRY } from 'src/app/modals/marketplace-settings/marketplace-settings.page' import { ConfigService } from 'src/app/services/config.service' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -20,16 +21,25 @@ export class MarketplaceListPage { readonly store$ = this.marketplaceService.getSelectedStore$().pipe( map(({ info, packages }) => { - const categories = new Set() - if (info.categories.includes('featured')) categories.add('featured') - info.categories.forEach(c => categories.add(c)) - categories.add('all') + const categories = new Map() - return { categories: Array.from(categories), packages } + categories.set('all', { + name: 'All', + description: { + short: 'All registry packages', + long: 'An unfiltered list of all packages available on this registry.', + }, + }) + + Object.keys(info.categories).forEach(c => + categories.set(c, info.categories[c]), + ) + + return { categories, packages } }), ) - readonly localPkgs$ = this.patch.watch$('package-data') + readonly localPkgs$ = this.patch.watch$('packageData') readonly details$ = this.marketplaceService.getSelectedHost$().pipe( map(({ url, name }) => { @@ -73,19 +83,20 @@ export class MarketplaceListPage { private readonly patch: PatchDB, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly config: ConfigService, private readonly route: ActivatedRoute, ) {} - category = 'featured' + category = 'all' query = '' async presentModalMarketplaceSettings() { - const modal = await this.modalCtrl.create({ - component: MarketplaceSettingsPage, - }) - await modal.present() + this.dialogs + .open(MARKETPLACE_REGISTRY, { + label: 'Change Registry', + }) + .subscribe() } onCategoryChange(category: string): void { diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts index d5d6304e4..cc4946dc1 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts @@ -17,13 +17,6 @@ const routes: Routes = [ m => m.MarketplaceShowPageModule, ), }, - { - path: ':pkgId/notes', - loadChildren: () => - import('./release-notes/release-notes.module').then( - m => m.ReleaseNotesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html index b150d2d4f..6178ebccc 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html @@ -1,33 +1,42 @@
- View Installed + {{ + localPkg.stateInfo.state === 'installed' + ? 'View Installed' + : 'View Installing' + }} - - - - Update - - + + - Downgrade - - + Update + + + Downgrade + + - - - - Install - - + + + {{ localFlavor ? 'Switch' : 'Install' }} + + +
diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index c2edfc15d..ab2efa6c7 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -4,29 +4,28 @@ import { Inject, Input, } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' +import { AlertController } from '@ionic/angular' import { AbstractMarketplaceService, MarketplacePkg, } from '@start9labs/marketplace' import { - Emver, - ErrorToastService, + Exver, + ErrorService, isEmptyObject, + LoadingService, sameUrl, } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { firstValueFrom } from 'rxjs' +import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel, PackageDataEntry, - PackageState, } from 'src/app/services/patch-db/data-model' -import { ClientStorageService } from 'src/app/services/client-storage.service' -import { MarketplaceService } from 'src/app/services/marketplace.service' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { PatchDB } from 'patch-db-client' -import { getAllPackages } from 'src/app/util/get-package-data' -import { firstValueFrom } from 'rxjs' import { dryUpdate } from 'src/app/util/dry-update' +import { getAllPackages, getManifest } from 'src/app/util/get-package-data' +import { hasCurrentDeps } from 'src/app/util/has-deps' @Component({ selector: 'marketplace-show-controls', @@ -44,25 +43,22 @@ export class MarketplaceShowControlsComponent { @Input() localPkg!: PackageDataEntry | null - readonly showDevTools$ = this.ClientStorageService.showDevTools$ + @Input() + localFlavor!: boolean - readonly PackageState = PackageState + @Input() + conflict?: string | null constructor( private readonly alertCtrl: AlertController, - private readonly ClientStorageService: ClientStorageService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly loadingCtrl: LoadingController, - private readonly emver: Emver, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly exver: Exver, + private readonly errorService: ErrorService, private readonly patch: PatchDB, ) {} - get localVersion(): string { - return this.localPkg?.manifest.version || '' - } - async tryInstall() { const currentMarketplace = await firstValueFrom( this.marketplaceService.getSelectedHost$(), @@ -72,7 +68,7 @@ export class MarketplaceShowControlsComponent { if (!this.localPkg) { this.alertInstall(url) } else { - const originalUrl = this.localPkg.installed?.['marketplace-url'] + const originalUrl = this.localPkg.registry if (!sameUrl(url, originalUrl)) { const proceed = await this.presentAlertDifferentMarketplace( @@ -82,10 +78,12 @@ export class MarketplaceShowControlsComponent { if (!proceed) return } + const localManifest = getManifest(this.localPkg) + if ( - this.emver.compare(this.localVersion, this.pkg.manifest.version) !== + this.exver.compareExver(localManifest.version, this.pkg.version) !== 0 && - hasCurrentDeps(this.localPkg) + hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) ) { this.dryInstall(url) } else { @@ -102,12 +100,11 @@ export class MarketplaceShowControlsComponent { this.patch.watch$('ui', 'marketplace'), ) - const name: string = marketplaces['known-hosts'][url]?.name || url + const name: string = marketplaces.knownHosts[url]?.name || url let originalName: string | undefined if (originalUrl) { - originalName = - marketplaces['known-hosts'][originalUrl]?.name || originalUrl + originalName = marketplaces.knownHosts[originalUrl]?.name || originalUrl } return new Promise(async resolve => { @@ -141,9 +138,9 @@ export class MarketplaceShowControlsComponent { private async dryInstall(url: string) { const breakages = dryUpdate( - this.pkg.manifest, + this.pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -157,7 +154,7 @@ export class MarketplaceShowControlsComponent { } private async alertInstall(url: string) { - const installAlert = this.pkg.manifest.alerts.install + const installAlert = this.pkg.alerts.install if (!installAlert) return this.install(url) @@ -182,19 +179,16 @@ export class MarketplaceShowControlsComponent { } private async install(url: string) { - const loader = await this.loadingCtrl.create({ - message: 'Beginning Install...', - }) - await loader.present() + const loader = this.loader.open('Beginning Install...').subscribe() - const { id, version } = this.pkg.manifest + const { id, version } = this.pkg try { await this.marketplaceService.installPackage(id, version, url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html index a7e5f9cb6..631b45018 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html @@ -13,16 +13,16 @@



- {{ title }} version {{ version | displayEmver }} is compatible. + {{ title }} version {{ version }} is compatible. - {{ title }} version {{ version | displayEmver }} is NOT compatible. + {{ title }} version {{ version }} is NOT compatible.

diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts index 76c648867..7c39714f5 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts @@ -24,10 +24,10 @@ export class MarketplaceShowDependentComponent { constructor(@Inject(DOCUMENT) private readonly document: Document) {} get title(): string { - return this.pkg.manifest.title + return this.pkg.title } get version(): string { - return this.pkg.manifest.version + return this.pkg.version } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 3cef27e61..7d0d3995b 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { RouterModule, Routes } from '@angular/router' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, TextSpinnerComponentModule, @@ -13,12 +13,14 @@ import { AdditionalModule, DependenciesModule, PackageModule, + FlavorsModule, } from '@start9labs/marketplace' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceShowPage } from './marketplace-show.page' import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component' import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component' import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' const routes: Routes = [ { @@ -34,13 +36,15 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, MarketplaceStatusModule, PackageModule, AboutModule, DependenciesModule, AdditionalModule, + FlavorsModule, + UiPipeModule, ], declarations: [ MarketplaceShowPage, diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index f563e88d6..f18a22968 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -1,45 +1,51 @@ - + -
+
-

- {{ pkgId }} @{{ version === '*' ? 'latest' : version }} not found in - this registry -

+

{{ pkgId }} not found in this registry

- + + + + +

+
+
+
+
diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts index dcb6e48d1..9b2562395 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts @@ -1,11 +1,24 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { ActivatedRoute, Router } from '@angular/router' +import { convertBytes, Exver, getPkgId } from '@start9labs/shared' +import { + AbstractMarketplaceService, + MarketplacePkg, +} from '@start9labs/marketplace' import { PatchDB } from 'patch-db-client' -import { BehaviorSubject } from 'rxjs' -import { filter, shareReplay, switchMap } from 'rxjs/operators' +import { combineLatest, Observable } from 'rxjs' +import { + filter, + first, + map, + pairwise, + shareReplay, + startWith, + switchMap, +} from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' +import { getManifest } from 'src/app/util/get-package-data' +import { Version, VersionRange } from '@start9labs/start-sdk' @Component({ selector: 'marketplace-show', @@ -17,21 +30,138 @@ export class MarketplaceShowPage { readonly pkgId = getPkgId(this.route) readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - readonly loadVersion$ = new BehaviorSubject('*') + readonly localPkg$ = combineLatest([ + this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)), + this.route.queryParamMap, + ]).pipe( + map(([localPkg, paramMap]) => + this.exver.getFlavor(getManifest(localPkg).version) === + paramMap.get('flavor') + ? localPkg + : null, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ) - readonly localPkg$ = this.patch - .watch$('package-data', this.pkgId) - .pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true })) + readonly localFlavor$ = this.localPkg$.pipe( + map(pkg => !pkg), + startWith(false), + ) - readonly pkg$ = this.loadVersion$.pipe( - switchMap(version => - this.marketplaceService.getPackage$(this.pkgId, version, this.url), + readonly pkg$: Observable = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService.getPackage$( + this.pkgId, + paramMap.get('version'), + paramMap.get('flavor'), + this.url, + ), + ), + ) + + readonly conflict$: Observable = combineLatest([ + this.pkg$, + this.patch.watch$('packageData', this.pkgId).pipe( + map(pkg => getManifest(pkg).version), + pairwise(), + filter(([prev, curr]) => prev !== curr), + map(([_, curr]) => curr), + ), + this.patch.watch$('serverInfo').pipe(first()), + ]).pipe( + map(([pkg, localVersion, server]) => { + let conflicts: string[] = [] + + // OS version + if ( + !Version.parse(pkg.osVersion).satisfies( + VersionRange.parse(server.packageVersionCompat), + ) + ) { + const compare = Version.parse(pkg.osVersion).compare( + Version.parse(server.version), + ) + conflicts.push( + compare === 'greater' + ? `Minimum StartOS version ${pkg.osVersion}. Detected ${server.version}` + : `Version ${pkg.version} is outdated and cannot run newer versions of StartOS`, + ) + } + + // package version + if ( + localVersion && + pkg.sourceVersion && + !this.exver.satisfies(localVersion, pkg.sourceVersion) + ) { + conflicts.push( + `Currently installed version ${localVersion} cannot be upgraded to version ${pkg.version}. Try installing an older version first.`, + ) + } + + const { arch, ram, device } = pkg.hardwareRequirements + + // arch + if (arch && !arch.includes(server.arch)) { + conflicts.push( + `Arch ${server.arch} is not supported. Supported: ${arch.join( + ', ', + )}.`, + ) + } + + // ram + if (ram && ram > server.ram) { + conflicts.push( + `Minimum ${convertBytes( + ram, + )} of RAM required, detected ${convertBytes(server.ram)}.`, + ) + } + + // devices + conflicts.concat( + device + .filter(d => + server.devices.some( + sd => + d.class === sd.class && !new RegExp(d.pattern).test(sd.product), + ), + ) + .map(d => d.patternDescription), + ) + + return conflicts.join(' ') + }), + shareReplay({ bufferSize: 1, refCount: true }), + ) + + readonly flavors$ = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService.getSelectedStore$().pipe( + map(s => + s.packages.filter( + p => p.id === this.pkgId && p.flavor !== paramMap.get('flavor'), + ), + ), + filter(p => p.length > 0), + ), ), ) constructor( private readonly route: ActivatedRoute, + private readonly router: Router, private readonly patch: PatchDB, private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, ) {} + + updateVersion(version: string) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { version }, + queryParamsHandling: 'merge', + }) + } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html index b39e47de6..173fda66d 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html @@ -1,27 +1,36 @@ - -
+ +
Installed Update Available
-
+ +
Removing
-
+ +
Installing diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts index 05e36471b..6686b2842 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts @@ -1,8 +1,14 @@ import { Component, Input } from '@angular/core' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { - PackageDataEntry, - PackageState, -} from 'src/app/services/patch-db/data-model' + isInstalled, + isInstalling, + isUpdating, + isRemoving, + isRestoring, + getManifest, +} from 'src/app/util/get-package-data' +import { Exver } from '@start9labs/shared' @Component({ selector: 'marketplace-status', @@ -11,12 +17,24 @@ import { }) export class MarketplaceStatusComponent { @Input() version!: string + @Input() localPkg!: PackageDataEntry - @Input() localPkg?: PackageDataEntry - - PackageState = PackageState + isInstalled = isInstalled + isInstalling = isInstalling + isUpdating = isUpdating + isRemoving = isRemoving + isRestoring = isRestoring get localVersion(): string { - return this.localPkg?.manifest.version || '' + return getManifest(this.localPkg).version + } + + get sameFlavor(): boolean { + return ( + this.exver.getFlavor(this.version) === + this.exver.getFlavor(this.localVersion) + ) } + + constructor(private readonly exver: Exver) {} } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts index 8a7151f40..d95b919a2 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts @@ -1,17 +1,16 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule } from '@start9labs/shared' - -import { InstallProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' +import { ExverPipesModule } from '@start9labs/shared' +import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' import { MarketplaceStatusComponent } from './marketplace-status.component' @NgModule({ imports: [ CommonModule, IonicModule, - EmverPipesModule, - InstallProgressPipeModule, + ExverPipesModule, + InstallingProgressPipeModule, ], declarations: [MarketplaceStatusComponent], exports: [MarketplaceStatusComponent], diff --git a/web/projects/ui/src/app/pages/notifications/notifications.module.ts b/web/projects/ui/src/app/pages/notifications/notifications.module.ts index 5ff2b4572..4ec29702c 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.module.ts +++ b/web/projects/ui/src/app/pages/notifications/notifications.module.ts @@ -6,6 +6,7 @@ import { NotificationsPage } from './notifications.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { SharedPipesModule } from '@start9labs/shared' import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' const routes: Routes = [ { @@ -22,6 +23,7 @@ const routes: Routes = [ BadgeMenuComponentModule, SharedPipesModule, BackupReportPageModule, + UiPipeModule, ], declarations: [NotificationsPage], }) diff --git a/web/projects/ui/src/app/pages/notifications/notifications.page.html b/web/projects/ui/src/app/pages/notifications/notifications.page.html index f9e697107..a00fb76cb 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.page.html +++ b/web/projects/ui/src/app/pages/notifications/notifications.page.html @@ -86,9 +86,9 @@

- - - {{ $any(packageData[pkgId])?.manifest.title || pkgId }} - + + {{ packageData[pkgId] ? (packageData[pkgId] | + toManifest).title : pkgId }} - {{ not.title }} @@ -104,7 +104,7 @@

{{ truncate(not.message) }}

View Full Message

-

{{ not['created-at'] | date: 'medium' }}

+

{{ not.createdAt | date: 'medium' }}

{{ truncate(not.message) }}

View Report + View Details + + View Service diff --git a/web/projects/ui/src/app/pages/notifications/notifications.page.ts b/web/projects/ui/src/app/pages/notifications/notifications.page.ts index 71bf50370..8c5945b08 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.page.ts +++ b/web/projects/ui/src/app/pages/notifications/notifications.page.ts @@ -1,21 +1,21 @@ import { Component } from '@angular/core' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ActivatedRoute } from '@angular/router' +import { AlertController, ModalController } from '@ionic/angular' +import { + ErrorService, + LoadingService, + MarkdownComponent, +} from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { first } from 'rxjs' +import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page' import { - ServerNotifications, NotificationLevel, ServerNotification, + ServerNotifications, } from 'src/app/services/api/api.types' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActivatedRoute } from '@angular/router' -import { ErrorToastService } from '@start9labs/shared' -import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page' -import { PatchDB } from 'patch-db-client' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' -import { first } from 'rxjs' @Component({ selector: 'notifications', @@ -29,14 +29,14 @@ export class NotificationsPage { needInfinite = false fromToast = !!this.route.snapshot.queryParamMap.get('toast') readonly perPage = 40 - readonly packageData$ = this.patch.watch$('package-data').pipe(first()) + readonly packageData$ = this.patch.watch$('packageData').pipe(first()) constructor( private readonly embassyApi: ApiService, private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly route: ActivatedRoute, private readonly patch: PatchDB, ) {} @@ -66,26 +66,23 @@ export class NotificationsPage { return notifications } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } return [] } async delete(id: number, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteNotification({ id }) this.notifications.splice(index, 1) this.beforeCursor = this.notifications[this.notifications.length - 1]?.id } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -116,9 +113,21 @@ export class NotificationsPage { component: BackupReportPage, componentProps: { report: notification.data, - timestamp: notification['created-at'], + timestamp: notification.createdAt, + }, + }) + await modal.present() + } + + async presentModalMarkdown(not: ServerNotification<2>) { + const modal = await this.modalCtrl.create({ + componentProps: { + title: not.title, + content: not.data, }, + component: MarkdownComponent, }) + await modal.present() } @@ -141,7 +150,7 @@ export class NotificationsPage { } truncate(message: string): string { - return message.length <= 240 ? message : '...' + message.substr(-240) + return message.length <= 240 ? message : message.substring(0, 160) + '...' } getColor({ level }: ServerNotification): string { @@ -160,10 +169,7 @@ export class NotificationsPage { } private async deleteAll(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteAllNotifications({ @@ -172,9 +178,9 @@ export class NotificationsPage { this.notifications = [] this.beforeCursor = undefined } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/server-routes/acme/acme.module.ts b/web/projects/ui/src/app/pages/server-routes/acme/acme.module.ts new file mode 100644 index 000000000..f00171f05 --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/acme/acme.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { ACMEPage } from './acme.page' + +const routes: Routes = [ + { + path: '', + component: ACMEPage, + }, +] + +@NgModule({ + imports: [CommonModule, IonicModule, RouterModule.forChild(routes)], + declarations: [ACMEPage], +}) +export class ACMEPageModule {} diff --git a/web/projects/ui/src/app/pages/server-routes/acme/acme.page.html b/web/projects/ui/src/app/pages/server-routes/acme/acme.page.html new file mode 100644 index 000000000..ab7ad3a01 --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/acme/acme.page.html @@ -0,0 +1,53 @@ + + + + + + ACME + + + + + + + + +

+ Register with one or more ACME providers such as Let's Encrypt in + order to generate SSL (https) certificates on-demand for clearnet + hosting + + View instructions + +

+
+
+ + Saved Providers + + + + + + Add Provider + + + + + + +

{{ toAcmeName(provider.url) }}

+

Contact: {{ provider.contactString }}

+
+ + + + + + + + +
+
+
+
diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.scss b/web/projects/ui/src/app/pages/server-routes/acme/acme.page.scss similarity index 100% rename from web/projects/ui/src/app/modals/enum-list/enum-list.page.scss rename to web/projects/ui/src/app/pages/server-routes/acme/acme.page.scss diff --git a/web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts b/web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts new file mode 100644 index 000000000..5416194aa --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts @@ -0,0 +1,179 @@ +import { Component } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { DataModel } from '../../../services/patch-db/data-model' +import { FormDialogService } from '../../../services/form-dialog.service' +import { FormComponent } from '../../../components/form.component' +import { configBuilderToSpec } from '../../../util/configBuilderToSpec' +import { ISB, utils } from '@start9labs/start-sdk' +import { knownACME, toAcmeName } from 'src/app/util/acme' +import { map } from 'rxjs' + +@Component({ + selector: 'acme', + templateUrl: 'acme.page.html', + styleUrls: ['acme.page.scss'], +}) +export class ACMEPage { + readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme' + + acme$ = this.patch.watch$('serverInfo', 'acme').pipe( + map(acme => { + const providerUrls = Object.keys(acme) + return providerUrls.map(url => { + const contact = acme[url].contact.map(mailto => + mailto.replace('mailto:', ''), + ) + return { + url, + contact, + contactString: contact.join(', '), + } + }) + }), + ) + + toAcmeName = toAcmeName + + constructor( + private readonly loader: LoadingService, + private readonly errorService: ErrorService, + private readonly api: ApiService, + private readonly patch: PatchDB, + private readonly formDialog: FormDialogService, + ) {} + + async addAcme( + providers: { + url: string + contact: string[] + contactString: string + }[], + ) { + this.formDialog.open(FormComponent, { + label: 'Add ACME Provider', + data: { + spec: await configBuilderToSpec( + getAddAcmeSpec(providers.map(p => p.url)), + ), + buttons: [ + { + text: 'Save', + handler: async ( + val: ReturnType['_TYPE'], + ) => { + const providerUrl = + val.provider.selection === 'other' + ? val.provider.value.url + : val.provider.selection + + return this.saveAcme(providerUrl, val.contact) + }, + }, + ], + }, + }) + } + + async editAcme(provider: string, contact: string[]) { + this.formDialog.open(FormComponent, { + label: 'Edit ACME Provider', + data: { + spec: await configBuilderToSpec(editAcmeSpec), + buttons: [ + { + text: 'Save', + handler: async (val: typeof editAcmeSpec._TYPE) => + this.saveAcme(provider, val.contact), + }, + ], + value: { contact }, + }, + }) + } + + async removeAcme(provider: string) { + const loader = this.loader.open('Removing').subscribe() + + try { + await this.api.removeAcme({ provider }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async saveAcme(providerUrl: string, contact: string[]) { + console.log(providerUrl, contact) + const loader = this.loader.open('Saving').subscribe() + + try { + await this.api.initAcme({ + provider: new URL(providerUrl).href, + contact: contact.map(address => `mailto:${address}`), + }) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } +} + +const emailListSpec = ISB.Value.list( + ISB.List.text( + { + name: 'Contact Emails', + description: + 'Needed to obtain a certificate from a Certificate Authority', + minLength: 1, + }, + { + inputmode: 'email', + patterns: [utils.Patterns.email], + }, + ), +) + +function getAddAcmeSpec(providers: string[]) { + const availableAcme = knownACME.filter(acme => !providers.includes(acme.url)) + + return ISB.InputSpec.of({ + provider: ISB.Value.union( + { name: 'Provider', default: (availableAcme[0]?.url as any) || 'other' }, + ISB.Variants.of({ + ...availableAcme.reduce( + (obj, curr) => ({ + ...obj, + [curr.url]: { + name: curr.name, + spec: ISB.InputSpec.of({}), + }, + }), + {}, + ), + other: { + name: 'Other', + spec: ISB.InputSpec.of({ + url: ISB.Value.text({ + name: 'URL', + default: null, + required: true, + inputmode: 'url', + patterns: [utils.Patterns.url], + }), + }), + }, + }), + ), + contact: emailListSpec, + }) +} + +const editAcmeSpec = ISB.InputSpec.of({ + contact: emailListSpec, +}) diff --git a/web/projects/ui/src/app/pages/server-routes/email/email.module.ts b/web/projects/ui/src/app/pages/server-routes/email/email.module.ts new file mode 100644 index 000000000..f6b0c735d --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/email/email.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { TuiInputModule } from '@taiga-ui/kit' +import { + TuiNotificationModule, + TuiTextfieldControllerModule, +} from '@taiga-ui/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { EmailPage } from './email.page' +import { FormModule } from 'src/app/components/form/form.module' +import { IonicModule } from '@ionic/angular' +import { TuiErrorModule, TuiModeModule } from '@taiga-ui/core' +import { TuiAppearanceModule, TuiButtonModule } from '@taiga-ui/experimental' + +const routes: Routes = [ + { + path: '', + component: EmailPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + CommonModule, + FormsModule, + ReactiveFormsModule, + TuiButtonModule, + TuiInputModule, + FormModule, + TuiNotificationModule, + TuiTextfieldControllerModule, + TuiAppearanceModule, + TuiModeModule, + TuiErrorModule, + ], + declarations: [EmailPage], +}) +export class EmailPageModule {} diff --git a/web/projects/ui/src/app/pages/server-routes/email/email.page.html b/web/projects/ui/src/app/pages/server-routes/email/email.page.html new file mode 100644 index 000000000..5e0e58fa4 --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/email/email.page.html @@ -0,0 +1,70 @@ + + + Email + + + + + + + + + Fill out the form below to connect to an external SMTP server. With your + permission, installed services can use the SMTP server to send emails. To + grant permission to a particular service, visit that service's "Actions" + page. Not all services support sending emails. + + View instructions + + + +
+

SMTP Credentials

+ + + +
+
+

Send Test Email

+ + To Address + + + +
+
+
diff --git a/web/projects/ui/src/app/pages/server-routes/email/email.page.scss b/web/projects/ui/src/app/pages/server-routes/email/email.page.scss new file mode 100644 index 000000000..b15986fc9 --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/email/email.page.scss @@ -0,0 +1,9 @@ +form { + padding-top: 24px; + margin: auto; + max-width: 30rem; +} + +h3 { + display: flex; +} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/server-routes/email/email.page.ts b/web/projects/ui/src/app/pages/server-routes/email/email.page.ts new file mode 100644 index 000000000..e52bf32dd --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/email/email.page.ts @@ -0,0 +1,83 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { IST, inputSpec } from '@start9labs/start-sdk' +import { TuiDialogService } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' +import { switchMap, tap } from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormService } from 'src/app/services/form.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' + +@Component({ + selector: 'email-page', + templateUrl: './email.page.html', + styleUrls: ['./email.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EmailPage { + private readonly dialogs = inject(TuiDialogService) + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + private readonly formService = inject(FormService) + private readonly patch = inject>(PatchDB) + private readonly api = inject(ApiService) + + isSaved = false + testAddress = '' + + readonly spec: Promise = configBuilderToSpec( + inputSpec.constants.customSmtp, + ) + readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe( + tap(value => (this.isSaved = !!value)), + switchMap(async value => + this.formService.createForm(await this.spec, value), + ), + ) + + async save( + value: typeof inputSpec.constants.customSmtp._TYPE | null, + ): Promise { + const loader = this.loader.open('Saving...').subscribe() + + try { + if (value) { + await this.api.setSmtp(value) + this.isSaved = true + } else { + await this.api.clearSmtp({}) + this.isSaved = false + } + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) { + const loader = this.loader.open('Sending email...').subscribe() + + try { + await this.api.testSmtp({ + to: this.testAddress, + ...value, + }) + } catch (e: any) { + return this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + + this.dialogs + .open( + `A test email has been sent to ${this.testAddress}.

Check your spam folder and mark as not spam`, + { + label: 'Success', + size: 's', + }, + ) + .subscribe() + } +} diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts deleted file mode 100644 index 86e374b17..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { ExperimentalFeaturesPage } from './experimental-features.page' -import { EmverPipesModule } from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: ExperimentalFeaturesPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - EmverPipesModule, - ], - declarations: [ExperimentalFeaturesPage], -}) -export class ExperimentalFeaturesPageModule {} diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html deleted file mode 100644 index 0ca8c7d8e..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Experimental Features - - - - - - - - -

Reset Tor

-

- Resetting the Tor daemon on your server may resolve Tor connectivity - issues. -

-
-
- - - -

{{ server.zram ? 'Disable' : 'Enable' }} zram

-

- Zram creates compressed swap in memory, resulting in faster I/O for - low RAM devices -

-
-
-
-
diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.scss b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts deleted file mode 100644 index bf445250a..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { - AlertController, - LoadingController, - ToastController, -} from '@ionic/angular' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConfigService } from 'src/app/services/config.service' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' - -@Component({ - selector: 'experimental-features', - templateUrl: './experimental-features.page.html', - styleUrls: ['./experimental-features.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ExperimentalFeaturesPage { - readonly server$ = this.patch.watch$('server-info') - - constructor( - private readonly toastCtrl: ToastController, - private readonly patch: PatchDB, - private readonly config: ConfigService, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly errToast: ErrorToastService, - ) {} - - async presentAlertResetTor() { - const isTor = this.config.isTor() - const shared = - 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' - const alert = await this.alertCtrl.create({ - header: isTor ? 'Warning' : 'Confirm', - message: isTor - ? `You are currently connected over Tor. If you reset the Tor daemon, you will loose connectivity until it comes back online.

${shared}` - : `Reset Tor?

${shared}`, - inputs: [ - { - label: 'Wipe state', - type: 'checkbox', - value: 'wipe', - }, - ], - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Reset', - handler: (value: string[]) => { - this.resetTor(value.some(v => v === 'wipe')) - }, - cssClass: 'enter-click', - }, - ], - cssClass: isTor ? 'alert-warning-message' : '', - }) - await alert.present() - } - - async presentAlertZram(enabled: boolean) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: enabled - ? 'Are you sure you want to disable zram? It provides significant performance benefits on low RAM devices.' - : 'Enable zram? It will only make a difference on lower RAM devices.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: enabled ? 'Disable' : 'Enable', - handler: () => { - this.toggleZram(enabled) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - private async resetTor(wipeState: boolean) { - const loader = await this.loadingCtrl.create({ - message: 'Resetting Tor...', - }) - await loader.present() - - try { - await this.api.resetTor({ - 'wipe-state': wipeState, - reason: 'User triggered', - }) - const toast = await this.toastCtrl.create({ - header: 'Tor reset in progress', - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - private async toggleZram(enabled: boolean) { - const loader = await this.loadingCtrl.create({ - message: enabled ? 'Disabling zram...' : 'Enabling zram...', - }) - await loader.present() - - try { - await this.api.toggleZram({ enable: !enabled }) - const toast = await this.toastCtrl.create({ - header: `Zram ${enabled ? 'disabled' : 'enabled'}`, - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } -} diff --git a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html index b61412445..c6c16d28e 100644 --- a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html @@ -35,5 +35,5 @@

Download Root CA

- + diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html index 7440c9a77..ef22a587b 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html @@ -1,5 +1,5 @@ diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts index 8cb4f6916..da99e66a2 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts @@ -5,7 +5,7 @@ import { IonicModule } from '@ionic/angular' import { RestorePage } from './restore.component' import { SharedPipesModule } from '@start9labs/shared' import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module' -import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module' +import { BackupServerSelectModule } from 'src/app/modals/backup-server-select/backup-server-select.module' const routes: Routes = [ { @@ -21,7 +21,7 @@ const routes: Routes = [ RouterModule.forChild(routes), SharedPipesModule, BackupDrivesComponentModule, - AppRecoverSelectPageModule, + BackupServerSelectModule, ], declarations: [RestorePage], }) diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts index 24adff639..58f679908 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts @@ -1,22 +1,11 @@ import { Component } from '@angular/core' -import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' +import { ModalController } from '@ionic/angular' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { - BackupInfo, CifsBackupTarget, DiskBackupTarget, } from 'src/app/services/api/api.types' -import { AppRecoverSelectPage } from 'src/app/modals/app-recover-select/app-recover-select.page' -import * as argon2 from '@start9labs/argon2' +import { BackupServerSelectModal } from 'src/app/modals/backup-server-select/backup-server-select.page' @Component({ selector: 'restore', @@ -24,83 +13,15 @@ import * as argon2 from '@start9labs/argon2' styleUrls: ['./restore.component.scss'], }) export class RestorePage { - constructor( - private readonly modalCtrl: ModalController, - private readonly navCtrl: NavController, - private readonly embassyApi: ApiService, - private readonly loadingCtrl: LoadingController, - ) {} + constructor(private readonly modalCtrl: ModalController) {} - async presentModalPassword( + async presentModalSelectServer( target: MappedBackupTarget, ): Promise { - const options: GenericInputOptions = { - title: 'Password Required', - message: - 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', - label: 'Master Password', - placeholder: 'Enter master password', - useMask: true, - buttonText: 'Next', - submitFn: async (password: string) => { - const passwordHash = target.entry['embassy-os']?.['password-hash'] || '' - argon2.verify(passwordHash, password) - await this.restoreFromBackup(target, password) - }, - } - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', + componentProps: { target }, presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - private async restoreFromBackup( - target: MappedBackupTarget, - password: string, - oldPassword?: string, - ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Decrypting drive...', - }) - await loader.present() - - try { - const backupInfo = await this.embassyApi.getBackupInfo({ - 'target-id': target.id, - password, - }) - this.presentModalSelect(target.id, backupInfo, password, oldPassword) - } finally { - loader.dismiss() - } - } - - private async presentModalSelect( - id: string, - backupInfo: BackupInfo, - password: string, - oldPassword?: string, - ): Promise { - const modal = await this.modalCtrl.create({ - componentProps: { - id, - backupInfo, - password, - oldPassword, - }, - presentingElement: await this.modalCtrl.getTop(), - component: AppRecoverSelectPage, - }) - - modal.onWillDismiss().then(res => { - if (res.role === 'success') { - this.navCtrl.navigateRoot('/services') - } + component: BackupServerSelectModal, }) await modal.present() diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html index 1bbd5aa86..d70291de9 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html @@ -15,9 +15,9 @@ - + - {{ pkg.value.manifest.title }} + {{ (pkg.value | toManifest).title }} diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts index 85ffbb2e1..7c7f381a1 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts @@ -6,11 +6,9 @@ import { } from '@angular/core' import { PatchDB } from 'patch-db-client' import { take } from 'rxjs/operators' -import { - DataModel, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { Observable } from 'rxjs' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'backing-up', @@ -18,15 +16,13 @@ import { Observable } from 'rxjs' changeDetection: ChangeDetectionStrategy.OnPush, }) export class BackingUpComponent { - readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1)) + readonly pkgs$ = this.patch.watch$('packageData').pipe(take(1)) readonly backupProgress$ = this.patch.watch$( - 'server-info', - 'status-info', - 'backup-progress', + 'serverInfo', + 'statusInfo', + 'backupProgress', ) - PackageMainStatus = PackageMainStatus - constructor(private readonly patch: PatchDB) {} } @@ -34,15 +30,8 @@ export class BackingUpComponent { name: 'pkgMainStatus', }) export class PkgMainStatusPipe implements PipeTransform { - transform(pkgId: string): Observable { - return this.patch.watch$( - 'package-data', - pkgId, - 'installed', - 'status', - 'main', - 'status', - ) + transform(pkgId: string): Observable { + return this.patch.watch$('packageData', pkgId, 'status', 'main') } constructor(private readonly patch: PatchDB) {} diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts index 6a1782985..d5b32cd6e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts @@ -8,6 +8,7 @@ import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/ba import { SharedPipesModule } from '@start9labs/shared' import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module' import { PkgMainStatusPipe } from './backing-up/backing-up.component' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' const routes: Routes = [ { @@ -24,6 +25,7 @@ const routes: Routes = [ SharedPipesModule, BackupDrivesComponentModule, BackupSelectPageModule, + UiPipeModule, ], declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe], }) diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.scss b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 2d4d3901e..9a067bc1e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -1,14 +1,11 @@ import { Component } from '@angular/core' +import { ModalController, NavController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' + PasswordPromptComponent, + PromptOptions, +} from 'src/app/modals/password-prompt.component' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' import { PatchDB } from 'patch-db-client' import { skip, takeUntil } from 'rxjs/operators' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' @@ -22,11 +19,11 @@ import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.pag import { EOSService } from 'src/app/services/eos.service' import { getServerInfo } from 'src/app/util/get-server-info' import { DataModel } from 'src/app/services/patch-db/data-model' +import { BackupService } from 'src/app/components/backup-drives/backup.service' @Component({ selector: 'server-backup', templateUrl: './server-backup.page.html', - styleUrls: ['./server-backup.page.scss'], providers: [TuiDestroyService], }) export class ServerBackupPage { @@ -35,13 +32,15 @@ export class ServerBackupPage { readonly backingUp$ = this.eosService.backingUp$ constructor( - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, private readonly destroy$: TuiDestroyService, private readonly eosService: EOSService, private readonly patch: PatchDB, + private readonly backupService: BackupService, ) {} ngOnInit() { @@ -62,7 +61,7 @@ export class ServerBackupPage { component: BackupSelectPage, }) - modal.onWillDismiss().then(res => { + modal.onDidDismiss().then(res => { if (res.data) { this.serviceIds = res.data this.presentModalPassword(target) @@ -75,78 +74,88 @@ export class ServerBackupPage { private async presentModalPassword( target: MappedBackupTarget, ): Promise { - const options: GenericInputOptions = { + const options: PromptOptions = { title: 'Master Password Needed', message: 'Enter your master password to encrypt this backup.', label: 'Master Password', placeholder: 'Enter master password', - useMask: true, buttonText: 'Create Backup', - submitFn: async (password: string) => { + } + + const modal = await this.modalCtrl.create({ + component: PasswordPromptComponent, + componentProps: { options }, + canDismiss: async password => { + if (password === null) { + return true + } + + const { passwordHash, id } = await getServerInfo(this.patch) + // confirm password matches current master password - const { 'password-hash': passwordHash } = await getServerInfo( - this.patch, - ) - argon2.verify(passwordHash, password) + try { + argon2.verify(passwordHash, password) + } catch (e: any) { + this.errorService.handleError(e) + return false + } // first time backup - if (!target.hasValidBackup) { - await this.createBackup(target, password) + if (!this.backupService.hasThisBackup(target.entry, id)) { + this.createBackup(target, password) + return true // existing backup } else { try { - const passwordHash = - target.entry['embassy-os']?.['password-hash'] || '' - - argon2.verify(passwordHash, password) + argon2.verify(target.entry.startOs[id].passwordHash!, password) } catch { setTimeout( () => this.presentModalOldPassword(target, password), - 500, + 250, ) - return + return true } await this.createBackup(target, password) + return true } }, - } - - const m = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', }) - - await m.present() + modal.present() } private async presentModalOldPassword( target: MappedBackupTarget, password: string, ): Promise { - const options: GenericInputOptions = { + const { id } = await getServerInfo(this.patch) + const options: PromptOptions = { title: 'Original Password Needed', message: 'This backup was created with a different password. Enter the ORIGINAL password that was used to encrypt this backup.', label: 'Original Password', placeholder: 'Enter original password', - useMask: true, buttonText: 'Create Backup', - submitFn: async (oldPassword: string) => { - const passwordHash = target.entry['embassy-os']?.['password-hash'] || '' - - argon2.verify(passwordHash, oldPassword) - await this.createBackup(target, password, oldPassword) - }, } - const m = await this.modalCtrl.create({ - component: GenericInputComponent, + const modal = await this.modalCtrl.create({ + component: PasswordPromptComponent, componentProps: { options }, - cssClass: 'alertlike-modal', - }) + canDismiss: async oldPassword => { + if (oldPassword === null) { + return true + } - await m.present() + try { + argon2.verify(target.entry.startOs[id].passwordHash!, oldPassword) + await this.createBackup(target, password, oldPassword) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } + }, + }) + modal.present() } private async createBackup( @@ -154,20 +163,17 @@ export class ServerBackupPage { password: string, oldPassword?: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Beginning backup...', - }) - await loader.present() + const loader = this.loader.open('Beginning backup...').subscribe() try { await this.embassyApi.createBackup({ - 'target-id': target.id, - 'package-ids': this.serviceIds, - 'old-password': oldPassword || null, + targetId: target.id, + packageIds: this.serviceIds, + oldPassword: oldPassword || null, password, }) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html index 0926334aa..859d48129 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -74,7 +74,7 @@

Memory Percentage Used - {{ memory['percentage-used'].value }} % + {{ memory.percentageUsed.value }} % Total @@ -94,15 +94,15 @@

zram Used - {{ memory['zram-used'].value }} MiB + {{ memory.zramUsed.value }} MiB zram Total - {{ memory['zram-total'].value }} MiB + {{ memory.zramTotal.value }} MiB zram Available - {{ memory['zram-available'].value }} MiB + {{ memory.available.value }} MiB @@ -110,18 +110,18 @@

CPU Percentage Used - {{ cpu['percentage-used'].value }} % + {{ cpu.percentageUsed.value }} % User Space - {{ cpu['user-space'].value }} % + {{ cpu.userSpace.value }} % Kernel Space - {{ cpu['kernel-space'].value }} % + {{ cpu.kernelSpace.value }} % @@ -138,7 +138,7 @@

Disk Percentage Used - {{ disk['percentage-used'].value }} % + {{ disk.percentageUsed.value }} % Capacity diff --git a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 569d34a45..4678fe3ea 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' +import { ErrorService, pauseFor } from '@start9labs/shared' +import { Subject } from 'rxjs' import { Metrics } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { TimeService } from 'src/app/services/time-service' -import { pauseFor, ErrorToastService } from '@start9labs/shared' -import { Subject } from 'rxjs' @Component({ selector: 'server-metrics', @@ -18,7 +18,7 @@ export class ServerMetricsPage { readonly uptime$ = this.timeService.uptime$ constructor( - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly timeService: TimeService, ) {} @@ -50,7 +50,7 @@ export class ServerMetricsPage { const metrics = await this.embassyApi.getServerMetrics({}) this.metrics$.next(metrics) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) this.stopDaemon() } } diff --git a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts index 5c728e668..e9e3163f9 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -81,11 +81,14 @@ const routes: Routes = [ import('./wifi/wifi.module').then(m => m.WifiPageModule), }, { - path: 'experimental-features', + path: 'email', loadChildren: () => - import('./experimental-features/experimental-features.module').then( - m => m.ExperimentalFeaturesPageModule, - ), + import('./email/email.module').then(m => m.EmailPageModule), + }, + { + path: 'acme', + loadChildren: () => + import('./acme/acme.module').then(m => m.ACMEPageModule), }, ] diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 760a013e6..d6adc15d1 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -16,7 +16,7 @@ @@ -40,27 +40,6 @@

Clock sync failure

- - - -

Http detected

-

- Tor is faster over https. - - Download and trust your server's Root CA - - , then switch to https. -

-
- - Open Https - - -
-
@@ -71,7 +50,7 @@

Http detected

Http detected

{{ button.title }}

{{ button.description }}

-

- + - Last Backup: {{ server['last-backup'] ? (server['last-backup'] | - date: 'medium') : 'never' }} + Last Backup: {{ server.lastBackup ? (server.lastBackup | date: + 'medium') : 'never' }} - + {{ button.title }}

diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 2b64fcf1c..be5945265 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -1,32 +1,32 @@ import { Component, Inject } from '@angular/core' +import { ActivatedRoute } from '@angular/router' import { AlertController, - LoadingController, ModalController, NavController, ToastController, } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ActivatedRoute } from '@angular/router' +import { WINDOW } from '@ng-web-apis/common' +import * as argon2 from '@start9labs/argon2' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { ISB } from '@start9labs/start-sdk' +import { TuiAlertService, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' -import { firstValueFrom, Observable, of } from 'rxjs' -import { ErrorToastService } from '@start9labs/shared' -import { EOSService } from 'src/app/services/eos.service' -import { ClientStorageService } from 'src/app/services/client-storage.service' +import { filter, from, Observable, of, switchMap } from 'rxjs' +import { take } from 'rxjs/operators' +import { FormComponent } from 'src/app/components/form.component' import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page' -import { getAllPackages } from '../../../util/get-package-data' +import { PROMPT } from 'src/app/modals/prompt.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' +import { ClientStorageService } from 'src/app/services/client-storage.service' import { ConfigService } from 'src/app/services/config.service' -import { WINDOW } from '@ng-web-apis/common' +import { EOSService } from 'src/app/services/eos.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' import { getServerInfo } from 'src/app/util/get-server-info' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import * as argon2 from '@start9labs/argon2' @Component({ selector: 'server-show', @@ -37,17 +37,18 @@ export class ServerShowPage { manageClicks = 0 powerClicks = 0 - readonly server$ = this.patch.watch$('server-info') + readonly server$ = this.patch.watch$('serverInfo') readonly showUpdate$ = this.eosService.showUpdate$ readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$ - readonly isTorHttp = this.config.isTorHttp() - constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly alerts: TuiAlertService, + private readonly dialogs: TuiDialogService, + private readonly formDialog: FormDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, private readonly route: ActivatedRoute, @@ -61,123 +62,174 @@ export class ServerShowPage { ) {} async setBrowserTab(): Promise { - const chosenName = await firstValueFrom(this.patch.watch$('ui', 'name')) - - const options: GenericInputOptions = { - title: 'Browser Tab Title', - message: `This value will be displayed as the title of your browser tab.`, - label: 'Device Name', - useMask: false, - placeholder: 'StartOS', - nullable: true, - initialValue: chosenName, - buttonText: 'Save', - submitFn: (name: string) => this.setName(name || null), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() + this.patch + .watch$('ui', 'name') + .pipe( + switchMap(initialValue => + this.dialogs.open(PROMPT, { + label: 'Browser Tab Title', + data: { + message: `This value will be displayed as the title of your browser tab.`, + label: 'Device Name', + placeholder: 'StartOS', + required: false, + buttonText: 'Save', + initialValue, + }, + }), + ), + take(1), + ) + .subscribe(async name => { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.embassyApi.setDbValue( + ['name'], + name || null, + ) + } finally { + loader.unsubscribe() + } + }) } async presentAlertResetPassword() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - 'You will still need your current password to decrypt existing backups!', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: + 'You will still need your current password to decrypt existing backups!', + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => this.presentModalResetPassword(), - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() - } - - async presentModalResetPassword(): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Change Master Password', - spec: PasswordSpec, - buttons: [ - { - text: 'Save', - handler: (value: any) => { - return this.resetPassword(value) - }, - isSubmit: true, + }) + .pipe( + filter(Boolean), + switchMap(() => from(configBuilderToSpec(passwordSpec))), + ) + .subscribe(spec => { + this.formDialog.open(FormComponent, { + label: 'Change Master Password', + data: { + spec, + buttons: [ + { + text: 'Save', + handler: (value: PasswordSpec) => this.resetPassword(value), + }, + ], }, - ], - }, - }) - await modal.present() + }) + }) } - private async resetPassword(value: { - currPass: string - newPass: string - newPass2: string - }): Promise { + private async resetPassword(value: PasswordSpec): Promise { let err = '' - if (value.newPass !== value.newPass2) { + if (value.newPassword1 !== value.newPassword2) { err = 'New passwords do not match' - } else if (value.newPass.length < 12) { + } else if (value.newPassword1.length < 12) { err = 'New password must be 12 characters or greater' - } else if (value.newPass.length > 64) { + } else if (value.newPassword1.length > 64) { err = 'New password must be less than 65 characters' } // confirm current password is correct - const { 'password-hash': passwordHash } = await getServerInfo(this.patch) + const { passwordHash } = await getServerInfo(this.patch) try { - argon2.verify(passwordHash, value.currPass) + argon2.verify(passwordHash, value.currentPassword) } catch (e) { err = 'Current password is invalid' } if (err) { - this.errToast.present(err) + this.errorService.handleError(err) return false } - const loader = await this.loadingCtrl.create({ - message: 'Changing master password...', - }) - await loader.present() + const loader = this.loader.open('Saving...').subscribe() try { await this.embassyApi.resetPassword({ - 'old-password': value.currPass, - 'new-password': value.newPass, - }) - const toast = await this.toastCtrl.create({ - header: 'Password changed!', - position: 'bottom', - duration: 2000, + oldPassword: value.currentPassword, + newPassword: value.newPassword1, }) - toast.present() + this.alerts.open('Password changed!').subscribe() + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() + } + } + + async presentAlertResetTor() { + const isTor = this.config.isTor() + const shared = + 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' + const alert = await this.alertCtrl.create({ + header: isTor ? 'Warning' : 'Confirm', + message: isTor + ? `You are currently connected over Tor. If you reset the Tor daemon, you will lose connectivity until it comes back online.

${shared}` + : `Reset Tor?

${shared}`, + inputs: [ + { + label: 'Wipe state', + type: 'checkbox', + value: 'wipe', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Reset', + handler: (value: string[]) => { + this.resetTor(value.some(v => v === 'wipe')) + }, + cssClass: 'enter-click', + }, + ], + cssClass: isTor ? 'alert-warning-message' : '', + }) + await alert.present() + } + + private async resetTor(wipeState: boolean) { + const loader = this.loader.open('Resetting Tor...').subscribe() + + try { + await this.embassyApi.resetTor({ + wipeState: wipeState, + reason: 'User triggered', + }) + const toast = await this.toastCtrl.create({ + header: 'Tor reset in progress', + position: 'bottom', + duration: 4000, + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => { + return true + }, + }, + ], + }) + await toast.present() + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() } } @@ -253,30 +305,6 @@ export class ServerShowPage { await alert.present() } - async presentAlertSystemRebuild() { - const localPkgs = await getAllPackages(this.patch) - const minutes = Object.keys(localPkgs).length * 2 - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Rebuild', - handler: () => { - this.systemRebuild() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() - } - async presentAlertRepairDisk() { const alert = await this.alertCtrl.create({ header: 'Warning', @@ -294,7 +322,7 @@ export class ServerShowPage { this.restart() }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } }, cssClass: 'enter-click', @@ -305,11 +333,6 @@ export class ServerShowPage { await alert.present() } - async launchHttps() { - const { 'tor-address': torAddress } = await getServerInfo(this.patch) - this.windowRef.open(torAddress, '_self') - } - addClick(title: string) { switch (title) { case 'Manage': @@ -323,19 +346,6 @@ export class ServerShowPage { } } - private async setName(value: string | null): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.embassyApi.setDbValue(['name'], value) - } finally { - loader.dismiss() - } - } - // should wipe cache independent of actual BE logout private logout() { this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e)) @@ -344,65 +354,37 @@ export class ServerShowPage { private async restart() { const action = 'Restart' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.restartServer({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async shutdown() { const action = 'Shutdown' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.shutdownServer({}) } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - private async systemRebuild() { - const action = 'System Rebuild' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() - - try { - await this.embassyApi.systemRebuild({}) - } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async checkForEosUpdate(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Checking for updates', - }) - await loader.present() + const loader = this.loader.open('Checking for updates').subscribe() try { await this.eosService.loadEos() - await loader.dismiss() + await loader.unsubscribe() if (this.eosService.updateAvailable$.value) { this.updateEos() @@ -410,8 +392,8 @@ export class ServerShowPage { this.presentAlertLatest() } } catch (e: any) { - await loader.dismiss() - this.errToast.present(e) + await loader.unsubscribe() + this.errorService.handleError(e) } } @@ -481,6 +463,24 @@ export class ServerShowPage { detail: true, disabled$: of(false), }, + { + title: 'ACME', + description: `Add ACME providers to create SSL certificates for clearnet access`, + icon: 'finger-print', + action: () => + this.navCtrl.navigateForward(['acme'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, + { + title: 'Email', + description: 'Connect to an external SMTP server for sending emails', + icon: 'mail-outline', + action: () => + this.navCtrl.navigateForward(['email'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, { title: 'SSH', description: @@ -493,10 +493,13 @@ export class ServerShowPage { }, { title: 'WiFi', - description: 'Add or remove WiFi networks', + description: + 'Connect your server to WiFi instead of Ethernet (not recommended)', icon: 'wifi', action: () => - this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['wifi'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, @@ -520,14 +523,11 @@ export class ServerShowPage { disabled$: of(false), }, { - title: 'Experimental Features', - description: 'Try out new and potentially unstable new features', - icon: 'flask-outline', - action: () => - this.navCtrl.navigateForward(['experimental-features'], { - relativeTo: this.route, - }), - detail: true, + title: 'Reset Tor', + description: 'May help resolve Tor connectivity issues.', + icon: 'reload-circle-outline', + action: () => this.presentAlertResetTor(), + detail: false, disabled$: of(false), }, ], @@ -660,14 +660,6 @@ export class ServerShowPage { detail: false, disabled$: of(false), }, - { - title: 'System Rebuild', - description: '', - icon: 'construct-outline', - action: () => this.presentAlertSystemRebuild(), - detail: false, - disabled$: of(false), - }, { title: 'Repair Disk', description: '', @@ -720,29 +712,25 @@ interface SettingBtn { disabled$: Observable } -const PasswordSpec: ConfigSpec = { - currPass: { - type: 'string', +const passwordSpec = ISB.InputSpec.of({ + currentPassword: ISB.Value.text({ name: 'Current Password', - placeholder: 'CurrentPass', - nullable: false, + required: true, + default: null, masked: true, - copyable: false, - }, - newPass: { - type: 'string', + }), + newPassword1: ISB.Value.text({ name: 'New Password', - placeholder: 'NewPass', - nullable: false, + required: true, + default: null, masked: true, - copyable: false, - }, - newPass2: { - type: 'string', + }), + newPassword2: ISB.Value.text({ name: 'Retype New Password', - placeholder: 'NewPass', - nullable: false, + required: true, + default: null, masked: true, - copyable: false, - }, -} + }), +}) + +export type PasswordSpec = typeof passwordSpec.validator._TYPE diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts index eff288eb2..d33876fe2 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts @@ -3,9 +3,10 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { ServerSpecsPage } from './server-specs.page' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { TuiLetModule } from '@taiga-ui/cdk' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' +import { InterfaceInfoModule } from 'src/app/components/interface-info/interface-info.module' const routes: Routes = [ { @@ -20,8 +21,9 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), QRComponentModule, - EmverPipesModule, + ExverPipesModule, TuiLetModule, + InterfaceInfoModule, ], declarations: [ServerSpecsPage], }) diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index 87dd8ef24..9f774b2ed 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -13,7 +13,7 @@

Version

-

{{ server.version | displayEmver }}

+

{{ server.version }}

@@ -25,82 +25,18 @@

Git Hash

- - Web Addresses - - -

Tor

-

{{ server['tor-address'] }}

-
-
- - - - - - -
-
- - -

LAN

-

{{ server['lan-address'] }}

-
- - - -
- - - -

{{ iface.key }} (IPv4)

-

{{ ipv4 || 'n/a' }}

-
- - - -
- - -

{{ iface.key }} (IPv6)

-

{{ ipv6 || 'n/a' }}

-
- - - -
-
- - Device Credentials -

CA fingerprint

-

{{ server['ca-fingerprint'] }}

+

{{ server.caFingerprint }}

- +
+ + Web Addresses + + diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts index c80003bb1..dd743b869 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts @@ -1,10 +1,31 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ModalController, ToastController } from '@ionic/angular' +import { ToastController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { ConfigService } from 'src/app/services/config.service' -import { QRComponent } from 'src/app/components/qr/qr.component' import { copyToClipboard } from '@start9labs/shared' import { DataModel } from 'src/app/services/patch-db/data-model' +import { map, Observable } from 'rxjs' +import { + getAddresses, + MappedInterface, +} from 'src/app/components/interface-info/interface-info.component' + +const iface = { + id: '', + name: 'StartOS User Interface', + description: + 'The primary user interface for your StartOS server, accessible from any browser.', + type: 'ui' as const, + masked: false, + addressInfo: { + hostId: '', + internalPort: 80, + scheme: 'http', + sslScheme: 'https', + suffix: '', + username: null, + }, +} @Component({ selector: 'server-specs', @@ -13,11 +34,18 @@ import { DataModel } from 'src/app/services/patch-db/data-model' changeDetection: ChangeDetectionStrategy.OnPush, }) export class ServerSpecsPage { - readonly server$ = this.patch.watch$('server-info') + readonly server$ = this.patch.watch$('serverInfo') + + readonly ui$: Observable = this.server$.pipe( + map(server => ({ + ...iface, + public: server.host.bindings[iface.addressInfo.internalPort].net.public, + addresses: getAddresses(iface, server.host, this.config), + })), + ) constructor( private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, private readonly patch: PatchDB, private readonly config: ConfigService, ) {} @@ -41,19 +69,4 @@ export class ServerSpecsPage { }) await toast.present() } - - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - asIsOrder(a: any, b: any) { - return 0 - } } diff --git a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html index 4e291b5fc..f1e7dda6b 100644 --- a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html @@ -52,10 +52,15 @@ >

{{ getPlatformName(currentSession.metadata.platforms) }}

-

- Last Active: {{ currentSession['last-active'] | date : 'medium' }} -

-

{{ currentSession['user-agent'] }}

+

{{ agent }}

+

+ First Seen + : {{ currentSession.loggedIn| date : 'medium' }} +

+

+ Last Active + : {{ currentSession.lastActive| date : 'medium' }} +

@@ -80,8 +85,15 @@

>

{{ getPlatformName(session.metadata.platforms) }}

-

Last Active: {{ session['last-active'] | date : 'medium' }}

-

{{ session['user-agent'] }}

+

{{ agent }}

+

+ First Seen + : {{ session.loggedIn| date : 'medium' }} +

+

+ Last Active + : {{ session.lastActive| date : 'medium' }} +

{ - return { - id, - ...session, - } - }) - .sort((a, b) => { - return ( - new Date(b['last-active']).valueOf() - - new Date(a['last-active']).valueOf() - ) - }) + .map(([id, session]) => ({ id, ...session })) + .sort( + (a, b) => + new Date(b.lastActive).valueOf() - new Date(a.lastActive).valueOf(), + ) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -68,18 +61,17 @@ export class SessionsPage { } async kill(ids: string[]): Promise { - const loader = await this.loadingCtrl.create({ - message: `Terminating session${ids.length > 1 ? 's' : ''}...`, - }) - await loader.present() + const loader = this.loader + .open(`Terminating session${ids.length > 1 ? 's' : ''}...`) + .subscribe() try { await this.embassyApi.killSessions({ ids }) this.otherSessions = this.otherSessions.filter(s => !ids.includes(s.id)) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -110,10 +102,6 @@ export class SessionsPage { return 'Unknown Device' } } - - asIsOrder(a: any, b: any) { - return 0 - } } interface SessionWithId extends Session { diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts index 863b2d127..27574da83 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts @@ -3,8 +3,9 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { SideloadPage } from './sideload.page' import { Routes, RouterModule } from '@angular/router' -import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared' +import { ExverPipesModule, SharedPipesModule } from '@start9labs/shared' import { DragNDropDirective } from './dnd.directive' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' const routes: Routes = [ { @@ -19,7 +20,8 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, + InstallingProgressPipeModule, ], declarations: [SideloadPage, DragNDropDirective], }) diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html index 6a54fe82e..abbb9128a 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html @@ -7,92 +7,121 @@ - + + + +

+ {{ phase.name }} + + : {{ progress }}% + +

+ +
+
+ -
- -

Upload .s9pk package file

-

- - Tip: switch to LAN for faster uploads. - -

- - - - -
- - -
-

- - - {{ uploadState?.message }} -

-
-
-
- - - -
-
- -

{{ toUpload.manifest.title }}

-

{{ toUpload.manifest.version | displayEmver }}

+ +
+ +

Upload .s9pk package file

+

+ + Tip: switch to LAN for faster uploads. + +

+ + + + +
+ + +
+

+ + + {{ uploadState?.message }} +

+
+
+
+ + + +
+
+ +

{{ toUpload.manifest.title }}

+

{{ toUpload.manifest.version }}

+
-
- - Try again - - - - Upload & Install + + Try again - -
+ + + Upload & Install + + +
+ diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts index baaf5a9ec..0a531b5f3 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -1,17 +1,20 @@ import { Component } from '@angular/core' -import { isPlatform, LoadingController, NavController } from '@ionic/angular' +import { isPlatform } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { S9pk } from '@start9labs/start-sdk' +import cbor from 'cbor' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { Manifest } from 'src/app/services/patch-db/data-model' import { ConfigService } from 'src/app/services/config.service' -import cbor from 'cbor' -import { ErrorToastService } from '@start9labs/shared' +import { SideloadService } from './sideload.service' +import { filter, firstValueFrom } from 'rxjs' interface Positions { [key: string]: [bigint, bigint] // [position, length] } const MAGIC = new Uint8Array([59, 59]) -const VERSION = new Uint8Array([1]) +const VERSION_1 = new Uint8Array([1]) +const VERSION_2 = new Uint8Array([2]) @Component({ selector: 'sideload', @@ -21,7 +24,7 @@ const VERSION = new Uint8Array([1]) export class SideloadPage { isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android') toUpload: { - manifest: Manifest | null + manifest: { title: string; version: string } | null icon: string | null file: File | null } = { @@ -35,12 +38,14 @@ export class SideloadPage { message: string } + readonly progress$ = this.sideloadService.progress$ + constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, - private readonly navCtrl: NavController, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly config: ConfigService, + private readonly sideloadService: SideloadService, ) {} handleFileDrop(e: any) { @@ -64,11 +69,36 @@ export class SideloadPage { async validateS9pk(file: File) { const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) - if (compare(magic, MAGIC) && compare(version, VERSION)) { - await this.parseS9pk(file) - return { - invalid: false, - message: 'A valid package file has been detected!', + if (compare(magic, MAGIC)) { + try { + if (compare(version, VERSION_1)) { + await this.parseS9pkV1(file) + return { + invalid: false, + message: 'A valid package file has been detected!', + } + } else if (compare(version, VERSION_2)) { + await this.parseS9pkV2(file) + return { + invalid: false, + message: 'A valid package file has been detected!', + } + } else { + console.error(version) + return { + invalid: true, + message: 'Invalid package file', + } + } + } catch (e) { + console.error(e) + return { + invalid: true, + message: + e instanceof Error + ? `Invalid package file: ${e.message}` + : 'Invalid package file', + } } } else { return { @@ -85,30 +115,22 @@ export class SideloadPage { } async handleUpload() { - const loader = await this.loadingCtrl.create({ - message: 'Uploading package', - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Starting upload').subscribe() + try { - const guid = await this.api.sideloadPackage({ - manifest: this.toUpload.manifest!, - icon: this.toUpload.icon!, - }) - this.api - .uploadPackage(guid, this.toUpload.file!) - .catch(e => console.error(e)) - - this.navCtrl.navigateRoot('/services') + const res = await this.api.sideloadPackage() + this.sideloadService.followProgress(res.progress) + await this.api.uploadPackage(res.upload, this.toUpload.file!) + await firstValueFrom(this.progress$) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() this.clearToUpload() } } - async parseS9pk(file: File) { + async parseS9pkV1(file: File) { const positions: Positions = {} // magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point let start = 103 @@ -118,11 +140,17 @@ export class SideloadPage { ).getUint32(0, false) await getPositions(start, end, file, positions, tocLength as any) - await this.getManifest(positions, file) - await this.getIcon(positions, file) + await this.getManifestV1(positions, file) + await this.getIconV1(positions, file) + } + + async parseS9pkV2(file: File) { + const s9pk = await S9pk.deserialize(file, null) + this.toUpload.manifest = s9pk.manifest + this.toUpload.icon = await s9pk.icon() } - async getManifest(positions: Positions, file: Blob) { + private async getManifestV1(positions: Positions, file: Blob) { const data = await blobToBuffer( file.slice( Number(positions['manifest'][0]), @@ -132,14 +160,11 @@ export class SideloadPage { this.toUpload.manifest = await cbor.decode(data, true) } - async getIcon(positions: Positions, file: Blob) { - const contentType = `image/${this.toUpload.manifest?.assets.icon - .split('.') - .pop()}` + private async getIconV1(positions: Positions, file: Blob) { const data = file.slice( Number(positions['icon'][0]), Number(positions['icon'][0]) + Number(positions['icon'][1]), - contentType, + '', ) this.toUpload.icon = await blobToDataURL(data) } @@ -227,6 +252,7 @@ async function readBlobToArrayBuffer( } function compare(a: Uint8Array, b: Uint8Array) { + if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false } diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts new file mode 100644 index 000000000..df6ec0868 --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts @@ -0,0 +1,57 @@ +import { inject, Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { ErrorService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + catchError, + EMPTY, + endWith, + shareReplay, + Subject, + switchMap, + tap, +} from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +@Injectable({ + providedIn: 'root', +}) +export class SideloadService { + private readonly guid$ = new Subject() + private readonly errorService = inject(ErrorService) + private readonly router = inject(Router) + + readonly progress$ = this.guid$.pipe( + switchMap(guid => + this.api + .openWebsocket$(guid, { + closeObserver: { + next: event => { + if (event.code !== 1000) { + this.errorService.handleError(event.reason) + } + }, + }, + }) + .pipe( + tap(p => { + if (p.overall === true) { + this.router.navigate([''], { replaceUrl: true }) + } + }), + endWith(null), + ), + ), + catchError(e => { + this.errorService.handleError('Websocket connection broken. Try again.') + return EMPTY + }), + shareReplay(1), + ) + + constructor(private readonly api: ApiService) {} + + followProgress(guid: string) { + this.guid$.next(guid) + } +} diff --git a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html index 4ac7d5439..4dd44dfd1 100644 --- a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html +++ b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html @@ -24,7 +24,7 @@

Saved Keys - + Add New Key @@ -62,18 +62,18 @@

- +

{{ ssh.hostname }}

-

{{ ssh['created-at'] | date: 'medium' }}

+

{{ ssh.createdAt| date: 'medium' }}

{{ ssh.alg }} {{ ssh.fingerprint }}

Remove diff --git a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index 476835566..6cef44cba 100644 --- a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts @@ -1,16 +1,12 @@ -import { Component } from '@angular/core' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' +import { ChangeDetectorRef, Component } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { filter } from 'rxjs' +import { take } from 'rxjs/operators' +import { PROMPT } from 'src/app/modals/prompt.component' import { SSHKey } from 'src/app/services/api/api.types' -import { ErrorToastService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' @Component({ selector: 'ssh-keys', @@ -23,10 +19,10 @@ export class SSHKeysPage { readonly docsUrl = 'https://docs.start9.com/0.3.5.x/user-manual/ssh' constructor( - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, + private readonly cdr: ChangeDetectorRef, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -38,89 +34,62 @@ export class SSHKeysPage { try { this.sshKeys = await this.embassyApi.getSshKeys({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async presentModalAdd() { - const { name, description } = sshSpec + add() { + this.dialogs + .open(PROMPT, ADD_OPTIONS) + .pipe(take(1)) + .subscribe(async key => { + const loader = this.loader.open('Saving...').subscribe() - const options: GenericInputOptions = { - title: name, - message: description, - label: name, - submitFn: (pk: string) => this.add(pk), - } - - const modal = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - await modal.present() + try { + this.sshKeys?.push(await this.embassyApi.addSshKey({ key })) + } finally { + loader.unsubscribe() + this.cdr.markForCheck() + } + }) } - async add(pubkey: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - const key = await this.embassyApi.addSshKey({ key: pubkey }) - this.sshKeys.push(key) - } finally { - loader.dismiss() - } - } + delete(key: SSHKey) { + this.dialogs + .open(TUI_PROMPT, DELETE_OPTIONS) + .pipe(filter(Boolean)) + .subscribe(async () => { + const loader = this.loader.open('Deleting...').subscribe() - async presentAlertDelete(i: number) { - const alert = await this.alertCtrl.create({ - header: 'Caution', - message: `Are you sure you want to delete this key?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.delete(i) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + try { + await this.embassyApi.deleteSshKey({ fingerprint: key.fingerprint }) + this.sshKeys?.splice(this.sshKeys?.indexOf(key), 1) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + this.cdr.markForCheck() + } + }) } +} - async delete(i: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() - - try { - const entry = this.sshKeys[i] - await this.embassyApi.deleteSshKey({ fingerprint: entry.fingerprint }) - this.sshKeys.splice(i, 1) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } +const ADD_OPTIONS: Partial> = { + label: 'SSH Key', + data: { + message: + 'Enter the SSH public key you would like to authorize for root access to your StartOS Server.', + }, } -const sshSpec = { - type: 'string', - name: 'SSH Key', - description: - 'Enter the SSH public key you would like to authorize for root access to your server.', - nullable: false, - masked: false, - copyable: false, +const DELETE_OPTIONS: Partial> = { + label: 'Confirm', + size: 's', + data: { + content: 'Delete key? This action cannot be undone.', + yes: 'Delete', + no: 'Cancel', + }, } diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 9a83774cb..ce5ceb56f 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -4,7 +4,7 @@ WiFi Settings - + Refresh @@ -13,162 +13,149 @@ - - - - - -

- Adding WiFi credentials to your StartOS allows you to remove the - Ethernet cable and move the device anywhere you want. StartOS will - automatically connect to available networks. - - View instructions - -

-
-
- - Country - - - - - - {{ wifi.country }} - {{ this.countries[wifi.country] }} - - Select Country - - - - - Saved Networks - - - - - - - - + + + + Country - Available Networks - - - - - - - - - - - - - - Saved Networks - + -
- - {{ ssid.key }} - - - - + + + {{ wifi.country }} - {{ this.countries[wifi.country] }} + + Select Country
- Available Networks - + + + Saved Networks + + + + + + + + + + Available Networks + + + + + + + + + + + + + + Saved Networks + - {{ avWifi.ssid }} +
+ + {{ ssid.key }}
+ + Available Networks + + + {{ avWifi.ssid }} + + + + + + + + + + Join Another Network + +
- - - - Join Another Network - - -
-
+ + +

No wireless interface detected.

+
+
diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index bac270613..7d59a4d30 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -1,19 +1,27 @@ -import { Component } from '@angular/core' +import { Component, Inject } from '@angular/core' import { ActionSheetController, AlertController, - LoadingController, - ModalController, ToastController, } from '@ionic/angular' -import { AlertInput } from '@ionic/core' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ActionSheetButton } from '@ionic/core' -import { ValueSpecObject } from 'src/app/pkg-config/config-types' +import { ActionSheetButton, AlertInput } from '@ionic/core' +import { WINDOW } from '@ng-web-apis/common' +import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { TuiDialogOptions } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' +import { FormComponent, FormContext } from 'src/app/components/form.component' import { RR } from 'src/app/services/api/api.types' -import { pauseFor, ErrorToastService } from '@start9labs/shared' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' +import { ConnectionService } from 'src/app/services/connection.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { DataModel } from 'src/app/services/patch-db/data-model' + +export interface WiFiForm { + ssid: string + password: string +} @Component({ selector: 'wifi', @@ -26,22 +34,34 @@ export class WifiPage { countries = require('../../../util/countries.json') as { [key: string]: string } + readonly hasWifi$ = this.patch.watch$('serverInfo', 'wifi', 'interface') constructor( private readonly api: ApiService, private readonly toastCtrl: ToastController, private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + private readonly errorService: ErrorService, private readonly actionCtrl: ActionSheetController, private readonly config: ConfigService, + private readonly patch: PatchDB, + readonly connection$: ConnectionService, + @Inject(WINDOW) private readonly windowRef: Window, ) {} async ngOnInit() { await this.getWifi() } + async openDocs() { + this.windowRef.open( + 'https://docs.start9.com/user-manual/wifi.html', + '_blank', + 'noreferrer', + ) + } + async getWifi(timeout: number = 0): Promise { this.loading = true try { @@ -50,7 +70,7 @@ export class WifiPage { await this.presentAlertCountry() } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -110,30 +130,25 @@ export class WifiPage { async presentModalAdd(ssid?: string, needsPW: boolean = true) { const wifiSpec = getWifiValueSpec(ssid, needsPW) - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: wifiSpec.name, + const options: Partial>> = { + label: wifiSpec.name, + data: { spec: wifiSpec.spec, buttons: [ { text: 'Save for Later', - handler: async (value: { ssid: string; password: string }) => { - await this.save(value.ssid, value.password) - }, + handler: async ({ ssid, password }) => this.save(ssid, password), }, { text: 'Save and Connect', - handler: async (value: { ssid: string; password: string }) => { - await this.saveAndConnect(value.ssid, value.password) - }, - isSubmit: true, + handler: async ({ ssid, password }) => + this.saveAndConnect(ssid, password), }, ], }, - }) + } - await modal.present() + this.formDialog.open(FormComponent, options) } async presentAction(ssid: string) { @@ -169,19 +184,16 @@ export class WifiPage { } private async setCountry(country: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Setting country...', - }) - await loader.present() + const loader = this.loader.open('Setting country...').subscribe() try { await this.api.setWifiCountry({ country }) await this.getWifi() this.wifi.country = country } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -260,43 +272,36 @@ export class WifiPage { } private async connect(ssid: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting. This could take a while...', - }) - await loader.present() + const loader = this.loader + .open('Connecting. This could take a while...') + .subscribe() try { await this.api.connectWifi({ ssid }) await this.confirmWifi(ssid) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async delete(ssid: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.api.deleteWifi({ ssid }) await this.getWifi() delete this.wifi.ssids[ssid] } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async save(ssid: string, password: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + const loader = this.loader.open('Saving...').subscribe() try { await this.api.addWifi({ @@ -307,17 +312,16 @@ export class WifiPage { }) await this.getWifi() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async saveAndConnect(ssid: string, password: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting. This could take a while...', - }) - await loader.present() + const loader = this.loader + .open('Connecting. This could take a while...') + .subscribe() try { await this.api.addWifi({ @@ -329,39 +333,62 @@ export class WifiPage { await this.confirmWifi(ssid, true) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } function getWifiValueSpec( - ssid?: string, + ssid: string | null = null, needsPW: boolean = true, -): ValueSpecObject { +): IST.ValueSpecObject { return { + warning: null, type: 'object', name: 'WiFi Credentials', description: 'Enter the network SSID and password. You can connect now or save the network for later.', spec: { ssid: { - type: 'string', + type: 'text', + minLength: null, + maxLength: null, + patterns: [], name: 'Network SSID', - nullable: false, + description: null, + inputmode: 'text', + placeholder: null, + required: true, masked: false, - copyable: false, default: ssid, + warning: null, + disabled: false, + immutable: false, + generate: null, }, password: { - type: 'string', + type: 'text', + minLength: null, + maxLength: null, + patterns: [ + { + regex: '^.{8,}$', + description: 'Must be longer than 8 characters', + }, + ], name: 'Password', - nullable: !needsPW, + description: null, + inputmode: 'text', + placeholder: null, + required: needsPW, masked: true, - copyable: false, - pattern: '^.{8,}$', - 'pattern-description': 'Must be longer than 8 characters', + default: null, + warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } diff --git a/web/projects/ui/src/app/pages/updates/updates.module.ts b/web/projects/ui/src/app/pages/updates/updates.module.ts index e07e798b4..4930463b0 100644 --- a/web/projects/ui/src/app/pages/updates/updates.module.ts +++ b/web/projects/ui/src/app/pages/updates/updates.module.ts @@ -5,16 +5,14 @@ import { RouterModule, Routes } from '@angular/router' import { FilterUpdatesPipe, UpdatesPage } from './updates.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { - EmverDisplayPipe, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, } from '@start9labs/shared' import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' import { RoundProgressModule } from 'angular-svg-round-progressbar' -import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' -import { MimeTypePipeModule } from '@start9labs/marketplace' const routes: Routes = [ { @@ -34,10 +32,9 @@ const routes: Routes = [ SkeletonListComponentModule, MarkdownPipeModule, RoundProgressModule, - InstallProgressPipeModule, + InstallingProgressPipeModule, StoreIconComponentModule, - EmverPipesModule, - MimeTypePipeModule, + ExverPipesModule, ], }) export class UpdatesPageModule {} diff --git a/web/projects/ui/src/app/pages/updates/updates.page.html b/web/projects/ui/src/app/pages/updates/updates.page.html index be48693f9..31c712d98 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.html +++ b/web/projects/ui/src/app/pages/updates/updates.page.html @@ -30,54 +30,50 @@ >
- + - + -

{{ pkg.manifest.title }}

+

{{ pkg.title }}

- - {{ local.installed?.manifest?.version || '' | - displayEmver }} - + {{ local.stateInfo.manifest.version }}     - - {{ pkg.manifest.version | displayEmver }} - + {{ pkg.version }}

-

+

{{ error }}

- + + + - {{ marketplaceService.updateErrors[pkg.manifest.id] ? - 'Retry' : 'Update' }} + {{ marketplaceService.updateErrors[pkg.id] ? 'Retry' : + 'Update' }} @@ -86,14 +82,12 @@

What's new
-

+

View listing diff --git a/web/projects/ui/src/app/pages/updates/updates.page.ts b/web/projects/ui/src/app/pages/updates/updates.page.ts index 522b51218..e25632bc8 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.ts +++ b/web/projects/ui/src/app/pages/updates/updates.page.ts @@ -2,28 +2,33 @@ import { Component, Inject } from '@angular/core' import { PatchDB } from 'patch-db-client' import { DataModel, + InstalledState, PackageDataEntry, + UpdatingState, } from 'src/app/services/patch-db/data-model' import { MarketplaceService } from 'src/app/services/marketplace.service' import { AbstractMarketplaceService, Marketplace, - MarketplaceManifest, MarketplacePkg, StoreIdentity, } from '@start9labs/marketplace' -import { Emver, isEmptyObject } from '@start9labs/shared' +import { Exver, isEmptyObject } from '@start9labs/shared' import { Pipe, PipeTransform } from '@angular/core' -import { combineLatest, Observable } from 'rxjs' +import { combineLatest, map, Observable } from 'rxjs' import { AlertController, NavController } from '@ionic/angular' import { hasCurrentDeps } from 'src/app/util/has-deps' -import { getAllPackages } from 'src/app/util/get-package-data' +import { + getAllPackages, + isInstalled, + isUpdating, +} from 'src/app/util/get-package-data' import { dryUpdate } from 'src/app/util/dry-update' interface UpdatesData { hosts: StoreIdentity[] marketplace: Marketplace - localPkgs: Record + localPkgs: Record> errors: string[] } @@ -36,7 +41,14 @@ export class UpdatesPage { readonly data$: Observable = combineLatest({ hosts: this.marketplaceService.getKnownHosts$(true), marketplace: this.marketplaceService.getMarketplace$(), - localPkgs: this.patch.watch$('package-data'), + localPkgs: this.patch.watch$('packageData').pipe( + map(pkgs => + Object.entries(pkgs).reduce((acc, [id, val]) => { + if (isInstalled(val) || isUpdating(val)) return { ...acc, [id]: val } + return acc + }, {} as Record>), + ), + ), errors: this.marketplaceService.getRequestErrors$(), }) @@ -46,7 +58,7 @@ export class UpdatesPage { private readonly patch: PatchDB, private readonly navCtrl: NavController, private readonly alertCtrl: AlertController, - private readonly emver: Emver, + private readonly exver: Exver, ) {} viewInMarketplace(event: Event, url: string, id: string) { @@ -57,33 +69,29 @@ export class UpdatesPage { }) } - async tryUpdate( - manifest: MarketplaceManifest, - url: string, - local: PackageDataEntry, - e: Event, - ): Promise { + async tryUpdate(pkg: MarketplacePkg, url: string, e: Event): Promise { e.stopPropagation() - const { id, version } = manifest + const { id, version } = pkg delete this.marketplaceService.updateErrors[id] this.marketplaceService.updateQueue[id] = true - if (hasCurrentDeps(local)) { - this.dryInstall(manifest, url) + // id OK because same as local id for update + if (hasCurrentDeps(id, await getAllPackages(this.patch))) { + this.dryInstall(pkg, url) } else { this.install(id, version, url) } } - private async dryInstall(manifest: MarketplaceManifest, url: string) { - const { id, version, title } = manifest + private async dryInstall(pkg: MarketplacePkg, url: string) { + const { id, version, title } = pkg const breakages = dryUpdate( - manifest, + pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -150,18 +158,22 @@ export class UpdatesPage { name: 'filterUpdates', }) export class FilterUpdatesPipe implements PipeTransform { - constructor(private readonly emver: Emver) {} + constructor(private readonly exver: Exver) {} transform( pkgs: MarketplacePkg[], - local: Record, + local: Record>, ): MarketplacePkg[] { - return pkgs.filter( - ({ manifest }) => - this.emver.compare( - manifest.version, - local[manifest.id]?.installed?.manifest.version || '', - ) === 1, - ) + return pkgs.filter(({ id, version, flavor }) => { + const localPkg = local[id] + return ( + localPkg && + this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor && + this.exver.compareExver( + version, + localPkg.stateInfo.manifest.version, + ) === 1 + ) + }) } } diff --git a/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts b/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts index e4f456c01..8586c3b35 100644 --- a/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts +++ b/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts @@ -5,10 +5,10 @@ import { DataModel, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info' import { combineLatest } from 'rxjs' import { DepErrorService } from 'src/app/services/dep-error.service' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'widget-health', @@ -26,12 +26,12 @@ export class HealthComponent { ] as const readonly data$ = combineLatest([ - inject(PatchDB).watch$('package-data'), + inject(PatchDB).watch$('packageData'), inject(DepErrorService).depErrors$, ]).pipe( map(([data, depErrors]) => { const pkgs = Object.values(data).map(pkg => - getPackageInfo(pkg, depErrors[pkg.manifest.id]), + getPackageInfo(pkg, depErrors[getManifest(pkg).id]), ) const result = this.labels.reduce>( (acc, label) => ({ @@ -55,14 +55,11 @@ export class HealthComponent { private getCount(label: string, pkgs: PkgInfo[]): number { switch (label) { case 'Error': - return pkgs.filter( - a => a.primaryStatus !== PrimaryStatus.Stopped && a.error, - ).length + return pkgs.filter(a => a.primaryStatus !== 'stopped' && a.error).length case 'Needs Attention': return pkgs.filter(a => a.warning).length case 'Stopped': - return pkgs.filter(a => a.primaryStatus === PrimaryStatus.Stopped) - .length + return pkgs.filter(a => a.primaryStatus === 'stopped').length case 'Transitioning': return pkgs.filter(a => a.transitioning).length default: diff --git a/web/projects/ui/src/app/pages/widgets/widgets.page.ts b/web/projects/ui/src/app/pages/widgets/widgets.page.ts index ca1541269..30d605bda 100644 --- a/web/projects/ui/src/app/pages/widgets/widgets.page.ts +++ b/web/projects/ui/src/app/pages/widgets/widgets.page.ts @@ -56,7 +56,7 @@ export class WidgetsPage { @Optional() @Inject(POLYMORPHEUS_CONTEXT) readonly context: TuiDialogContext | null, - private readonly dialog: TuiDialogService, + private readonly dialogs: TuiDialogService, private readonly patch: PatchDB, private readonly cdr: ChangeDetectorRef, private readonly api: ApiService, @@ -83,7 +83,7 @@ export class WidgetsPage { } add() { - this.dialog.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => { + this.dialogs.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => { this.addWidget(widget!) }) } diff --git a/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts b/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts index 37bbd0744..a0997f29c 100644 --- a/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts +++ b/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core' import { - InstallProgressDisplayPipe, - InstallProgressPipe, + InstallingProgressDisplayPipe, + InstallingProgressPipe, } from './install-progress.pipe' @NgModule({ - declarations: [InstallProgressPipe, InstallProgressDisplayPipe], - exports: [InstallProgressPipe, InstallProgressDisplayPipe], + declarations: [InstallingProgressPipe, InstallingProgressDisplayPipe], + exports: [InstallingProgressPipe, InstallingProgressDisplayPipe], }) -export class InstallProgressPipeModule {} +export class InstallingProgressPipeModule {} diff --git a/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts b/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts index 459dc722b..c6ae05a9c 100644 --- a/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts +++ b/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts @@ -1,24 +1,27 @@ import { Pipe, PipeTransform } from '@angular/core' -import { InstallProgress } from 'src/app/services/patch-db/data-model' -import { packageLoadingProgress } from 'src/app/util/package-loading-progress' +import { T } from '@start9labs/start-sdk' @Pipe({ - name: 'installProgress', + name: 'installingProgressString', }) -export class InstallProgressPipe implements PipeTransform { - transform(installProgress?: InstallProgress): number { - return packageLoadingProgress(installProgress)?.totalProgress || 0 +export class InstallingProgressDisplayPipe implements PipeTransform { + transform(progress: T.Progress): string { + if (progress === true) return 'finalizing' + if (progress === false || progress === null || !progress.total) + return 'unknown %' + const percentage = Math.round((100 * progress.done) / progress.total) + + return percentage < 99 ? String(percentage) + '%' : 'finalizing' } } @Pipe({ - name: 'installProgressDisplay', + name: 'installingProgress', }) -export class InstallProgressDisplayPipe implements PipeTransform { - transform(installProgress?: InstallProgress): string { - const totalProgress = - packageLoadingProgress(installProgress)?.totalProgress || 0 - - return totalProgress < 99 ? totalProgress + '%' : 'finalizing' +export class InstallingProgressPipe implements PipeTransform { + transform(progress: T.Progress): number { + if (progress === true) return 100 + if (progress === false || progress === null || !progress.total) return 0 + return Math.floor((100 * progress.done) / progress.total) } } diff --git a/web/projects/ui/src/app/pipes/launchable/launchable.pipe.ts b/web/projects/ui/src/app/pipes/launchable/launchable.pipe.ts index be1d5218d..405d61019 100644 --- a/web/projects/ui/src/app/pipes/launchable/launchable.pipe.ts +++ b/web/projects/ui/src/app/pipes/launchable/launchable.pipe.ts @@ -1,10 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core' -import { - InterfaceDef, - PackageMainStatus, - PackageState, -} from 'src/app/services/patch-db/data-model' import { ConfigService } from '../../services/config.service' +import { T } from '@start9labs/start-sdk' @Pipe({ name: 'isLaunchable', @@ -13,10 +9,9 @@ export class LaunchablePipe implements PipeTransform { constructor(private configService: ConfigService) {} transform( - state: PackageState, - status: PackageMainStatus, - interfaces: Record, + state: T.PackageState['state'], + status: T.MainStatus['main'], ): boolean { - return this.configService.isLaunchable(state, status, interfaces) + return this.configService.isLaunchable(state, status) } } diff --git a/web/projects/ui/src/app/pipes/ui/ui.module.ts b/web/projects/ui/src/app/pipes/ui/ui.module.ts index 9637306de..a031c2059 100644 --- a/web/projects/ui/src/app/pipes/ui/ui.module.ts +++ b/web/projects/ui/src/app/pipes/ui/ui.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core' -import { UiPipe } from './ui.pipe' +import { ToManifestPipe, UiPipe } from './ui.pipe' @NgModule({ - declarations: [UiPipe], - exports: [UiPipe], + declarations: [UiPipe, ToManifestPipe], + exports: [UiPipe, ToManifestPipe], }) export class UiPipeModule {} diff --git a/web/projects/ui/src/app/pipes/ui/ui.pipe.ts b/web/projects/ui/src/app/pipes/ui/ui.pipe.ts index 9d46bfd86..6994d1a30 100644 --- a/web/projects/ui/src/app/pipes/ui/ui.pipe.ts +++ b/web/projects/ui/src/app/pipes/ui/ui.pipe.ts @@ -1,12 +1,23 @@ import { Pipe, PipeTransform } from '@angular/core' -import { InterfaceDef } from '../../services/patch-db/data-model' +import { PackageDataEntry } from '../../services/patch-db/data-model' import { hasUi } from '../../services/config.service' +import { getManifest } from 'src/app/util/get-package-data' +import { T } from '@start9labs/start-sdk' @Pipe({ name: 'hasUi', }) export class UiPipe implements PipeTransform { - transform(interfaces: Record): boolean { - return hasUi(interfaces) + transform(interfaces: PackageDataEntry['serviceInterfaces']): boolean { + return interfaces ? hasUi(interfaces) : false + } +} + +@Pipe({ + name: 'toManifest', +}) +export class ToManifestPipe implements PipeTransform { + transform(pkg: PackageDataEntry): T.Manifest { + return getManifest(pkg) } } diff --git a/web/projects/ui/src/app/pkg-config/config-types.ts b/web/projects/ui/src/app/pkg-config/config-types.ts deleted file mode 100644 index 08f0b9d26..000000000 --- a/web/projects/ui/src/app/pkg-config/config-types.ts +++ /dev/null @@ -1,171 +0,0 @@ -export type ConfigSpec = Record - -export type ValueType = - | 'string' - | 'number' - | 'boolean' - | 'enum' - | 'list' - | 'object' - | 'pointer' - | 'union' -export type ValueSpec = ValueSpecOf - -// core spec types. These types provide the metadata for performing validations -export type ValueSpecOf = T extends 'string' - ? ValueSpecString - : T extends 'number' - ? ValueSpecNumber - : T extends 'boolean' - ? ValueSpecBoolean - : T extends 'enum' - ? ValueSpecEnum - : T extends 'list' - ? ValueSpecList - : T extends 'object' - ? ValueSpecObject - : T extends 'pointer' - ? ValueSpecPointer - : T extends 'union' - ? ValueSpecUnion - : never - -export interface ValueSpecString extends ListValueSpecString, WithStandalone { - type: 'string' - default?: DefaultString - nullable: boolean - textarea?: boolean -} - -export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone { - type: 'number' - nullable: boolean - default?: number -} - -export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone { - type: 'enum' - default: string -} - -export interface ValueSpecBoolean extends WithStandalone { - type: 'boolean' - default: boolean -} - -export interface ValueSpecUnion { - type: 'union' - tag: UnionTagSpec - variants: { [key: string]: ConfigSpec } - default: string -} - -export interface ValueSpecPointer extends WithStandalone { - type: 'pointer' - subtype: 'package' | 'system' - 'package-id': string - target: 'lan-address' | 'tor-address' | 'config' | 'tor-key' - interface: string // will only exist if target = tor-key || tor-address || lan-address - selector?: string // will only exist if target = config - multi?: boolean // will only exist if target = config -} - -export interface ValueSpecObject extends WithStandalone { - type: 'object' - spec: ConfigSpec -} - -export interface WithStandalone { - name: string - description?: string - warning?: string -} - -// no lists of booleans, lists, pointers -export type ListValueSpecType = - | 'string' - | 'number' - | 'enum' - | 'object' - | 'union' - -// represents a spec for the values of a list -export type ListValueSpecOf = T extends 'string' - ? ListValueSpecString - : T extends 'number' - ? ListValueSpecNumber - : T extends 'enum' - ? ListValueSpecEnum - : T extends 'object' - ? ListValueSpecObject - : T extends 'union' - ? ListValueSpecUnion - : never - -// represents a spec for a list -export type ValueSpecList = ValueSpecListOf -export interface ValueSpecListOf - extends WithStandalone { - type: 'list' - subtype: T - spec: ListValueSpecOf - range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules - default: string[] | number[] | DefaultString[] | object[] -} - -// sometimes the type checker needs just a little bit of help -export function isValueSpecListOf( - t: ValueSpecList, - s: S, -): t is ValueSpecListOf { - return t.subtype === s -} - -export interface ListValueSpecString { - pattern?: string - 'pattern-description'?: string - masked: boolean - copyable: boolean - placeholder?: string -} - -export interface ListValueSpecNumber { - range: string - integral: boolean - units?: string - placeholder?: string -} - -export interface ListValueSpecEnum { - values: string[] - 'value-names': { [value: string]: string } -} - -export interface ListValueSpecObject { - spec: ConfigSpec // this is a mapped type of the config object at this level, replacing the object's values with specs on those values - 'unique-by': UniqueBy // indicates whether duplicates can be permitted in the list - 'display-as'?: string // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' -} - -export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] } - -export interface ListValueSpecUnion { - tag: UnionTagSpec - variants: { [key: string]: ConfigSpec } - 'display-as'?: string // this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id - 'unique-by': UniqueBy - default: string // this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list -} - -export interface UnionTagSpec { - id: string // The name of the field containing one of the union variants - 'variant-names': { - // the name of each variant - [variant: string]: string - } - name: string - description?: string - warning?: string -} - -export type DefaultString = string | { charset: string; len: number } diff --git a/web/projects/ui/src/app/pkg-config/config-utilities.ts b/web/projects/ui/src/app/pkg-config/config-utilities.ts deleted file mode 100644 index 431bc1c5d..000000000 --- a/web/projects/ui/src/app/pkg-config/config-utilities.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { ValueSpec, DefaultString } from './config-types' - -export class Range { - min?: number - max?: number - minInclusive!: boolean - maxInclusive!: boolean - - static from(s: string): Range { - const r = new Range() - r.minInclusive = s.startsWith('[') - r.maxInclusive = s.endsWith(']') - const [minStr, maxStr] = s.split(',').map(a => a.trim()) - r.min = minStr === '(*' ? undefined : Number(minStr.slice(1)) - r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1)) - return r - } - - checkIncludes(n: number) { - if ( - this.hasMin() && - (this.min > n || (!this.minInclusive && this.min == n)) - ) { - throw new Error(this.minMessage()) - } - if ( - this.hasMax() && - (this.max < n || (!this.maxInclusive && this.max == n)) - ) { - throw new Error(this.maxMessage()) - } - } - - hasMin(): this is Range & { min: number } { - return this.min !== undefined - } - - hasMax(): this is Range & { max: number } { - return this.max !== undefined - } - - minMessage(): string { - return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}` - } - - maxMessage(): string { - return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}` - } - - description(): string { - let message = 'Value can be any number.' - - if (this.hasMin() || this.hasMax()) { - message = 'Value must be' - } - - if (this.hasMin() && this.hasMax()) { - message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.` - } else if (this.hasMin() && !this.hasMax()) { - message = `${message} ${this.minMessage()}.` - } else if (!this.hasMin() && this.hasMax()) { - message = `${message} ${this.maxMessage()}.` - } - - return message - } - - integralMin(): number | undefined { - if (this.min) { - const ceil = Math.ceil(this.min) - if (this.minInclusive) { - return ceil - } else { - if (ceil === this.min) { - return ceil + 1 - } else { - return ceil - } - } - } - } - - integralMax(): number | undefined { - if (this.max) { - const floor = Math.floor(this.max) - if (this.maxInclusive) { - return floor - } else { - if (floor === this.max) { - return floor - 1 - } else { - return floor - } - } - } - } -} - -export function getDefaultDescription(spec: ValueSpec): string { - let toReturn: string | undefined - switch (spec.type) { - case 'string': - if (typeof spec.default === 'string') { - toReturn = spec.default - } else if (typeof spec.default === 'object') { - toReturn = 'random' - } - break - case 'number': - if (typeof spec.default === 'number') { - toReturn = String(spec.default) - } - break - case 'boolean': - toReturn = spec.default === true ? 'True' : 'False' - break - case 'enum': - toReturn = spec['value-names'][spec.default] - break - } - - return toReturn || '' -} - -export function getDefaultString(defaultSpec: DefaultString): string { - if (typeof defaultSpec === 'string') { - return defaultSpec - } else { - let s = '' - for (let i = 0; i < defaultSpec.len; i++) { - s = s + getRandomCharInSet(defaultSpec.charset) - } - - return s - } -} - -// a,g,h,A-Z,,,,- -export function getRandomCharInSet(charset: string): string { - const set = stringToCharSet(charset) - let charIdx = Math.floor(Math.random() * set.len) - for (let range of set.ranges) { - if (range.len > charIdx) { - return String.fromCharCode(range.start.charCodeAt(0) + charIdx) - } - charIdx -= range.len - } - throw new Error('unreachable') -} - -function stringToCharSet(charset: string): CharSet { - let set: CharSet = { ranges: [], len: 0 } - let start: string | null = null - let end: string | null = null - let in_range = false - for (let char of charset) { - switch (char) { - case ',': - if (start !== null && end !== null) { - if (start!.charCodeAt(0) > end!.charCodeAt(0)) { - throw new Error('start > end of charset') - } - const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 - set.ranges.push({ - start, - end, - len, - }) - set.len += len - start = null - end = null - in_range = false - } else if (start !== null && !in_range) { - set.len += 1 - set.ranges.push({ start, end: start, len: 1 }) - start = null - } else if (start !== null && in_range) { - end = ',' - } else if (start === null && end === null && !in_range) { - start = ',' - } else { - throw new Error('unexpected ","') - } - break - case '-': - if (start === null) { - start = '-' - } else if (!in_range) { - in_range = true - } else if (in_range && end === null) { - end = '-' - } else { - throw new Error('unexpected "-"') - } - break - default: - if (start === null) { - start = char - } else if (in_range && end === null) { - end = char - } else { - throw new Error(`unexpected "${char}"`) - } - } - } - if (start !== null && end !== null) { - if (start!.charCodeAt(0) > end!.charCodeAt(0)) { - throw new Error('start > end of charset') - } - const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 - set.ranges.push({ - start, - end, - len, - }) - set.len += len - } else if (start !== null) { - set.len += 1 - set.ranges.push({ - start, - end: start, - len: 1, - }) - } - return set -} - -interface CharSet { - ranges: { - start: string - end: string - len: number - }[] - len: number -} diff --git a/web/projects/ui/src/app/services/action.service.ts b/web/projects/ui/src/app/services/action.service.ts new file mode 100644 index 000000000..298b9facc --- /dev/null +++ b/web/projects/ui/src/app/services/action.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@angular/core' +import { AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { + ActionInputModal, + PackageActionData, +} from '../modals/action-input.component' + +const allowedStatuses = { + 'only-running': new Set(['running']), + 'only-stopped': new Set(['stopped']), + any: new Set([ + 'running', + 'stopped', + 'restarting', + 'restoring', + 'stopping', + 'starting', + 'backingUp', + ]), +} + +@Injectable({ + providedIn: 'root', +}) +export class ActionService { + constructor( + private readonly api: ApiService, + private readonly dialogs: TuiDialogService, + private readonly alertCtrl: AlertController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + ) {} + + async present(data: PackageActionData) { + const { pkgInfo, actionInfo } = data + + if ( + allowedStatuses[actionInfo.metadata.allowedStatuses].has( + pkgInfo.mainStatus, + ) + ) { + if (actionInfo.metadata.hasInput) { + this.formDialog.open(ActionInputModal, { + label: actionInfo.metadata.name, + data, + }) + } else { + if (actionInfo.metadata.warning) { + const alert = await this.alertCtrl.create({ + header: 'Warning', + message: actionInfo.metadata.warning, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Run', + handler: () => { + this.execute(pkgInfo.id, actionInfo.id) + }, + cssClass: 'enter-click', + }, + ], + cssClass: 'alert-warning-message', + }) + await alert.present() + } else { + this.execute(pkgInfo.id, actionInfo.id) + } + } + } else { + const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]] + const last = statuses.pop() + let statusesStr = statuses.join(', ') + let error = '' + if (statuses.length) { + if (statuses.length > 1) { + // oxford comma + statusesStr += ',' + } + statusesStr += ` or ${last}` + } else if (last) { + statusesStr = `${last}` + } else { + error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.` + } + const alert = await this.alertCtrl.create({ + header: 'Forbidden', + message: + error || + `Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`, + buttons: ['OK'], + cssClass: 'alert-error-message enter-click', + }) + await alert.present() + } + } + + async execute( + packageId: string, + actionId: string, + input?: object, + ): Promise { + const loader = this.loader.open('Loading...').subscribe() + + try { + const res = await this.api.runAction({ + packageId, + actionId, + input: input || null, + }) + + if (!res) return true + + if (res.result) { + this.dialogs + .open(new PolymorpheusComponent(ActionSuccessPage), { + label: res.title, + data: res, + }) + .subscribe() + } else if (res.message) { + this.dialogs.open(res.message, { label: res.title }).subscribe() + } + return true // needed to dismiss original modal/alert + } catch (e: any) { + this.errorService.handleError(e) + return false // don't dismiss original modal/alert + } finally { + loader.unsubscribe() + } + } +} diff --git a/web/projects/ui/src/app/services/api/api-icons.ts b/web/projects/ui/src/app/services/api/api-icons.ts index 3b0b08b33..55c3172da 100644 --- a/web/projects/ui/src/app/services/api/api-icons.ts +++ b/web/projects/ui/src/app/services/api/api-icons.ts @@ -1,8 +1,10 @@ +const REGISTRY_ICON = + 'data:image/png;base64,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' const BTC_ICON = - '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' + 'data:image/png;base64,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' const LND_ICON = - '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' + 'data:image/png;base64,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' const PROXY_ICON = - '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' + 'data:image/png;base64,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' -export { BTC_ICON, LND_ICON, PROXY_ICON } +export { REGISTRY_ICON, BTC_ICON, LND_ICON, PROXY_ICON } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 17460609a..bcfedb091 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1,29 +1,40 @@ import { - DockerIoFormat, - Manifest, + InstalledState, PackageDataEntry, - PackageMainStatus, - PackageState, - ServerStatusInfo, } from 'src/app/services/patch-db/data-model' -import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' - -import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' -import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace' +import { NotificationLevel, RR, ServerNotifications } from './api.types' +import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons' import { Log } from '@start9labs/shared' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { T, ISB, IST } from '@start9labs/start-sdk' +import { GetPackagesRes } from '@start9labs/marketplace' + +import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' + +const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = { + rootSighash: 'fakehash', + rootMaxsize: 0, +} + +const mockDescription = { + short: 'Lorem ipsum dolor sit amet', + long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +} export module Mock { - export const ServerUpdated: ServerStatusInfo = { - 'backup-progress': null, - 'update-progress': null, + export const ServerUpdated: T.ServerStatus = { + backupProgress: null, + updateProgress: null, updated: true, restarting: false, - 'shutting-down': false, + shuttingDown: false, } - export const MarketplaceEos: RR.GetMarketplaceEosRes = { - version: '0.3.5.1', + export const MarketplaceEos: RR.CheckOSUpdateRes = { + version: '0.3.6', headline: 'Our biggest release ever.', - 'release-notes': { + releaseNotes: { + '0.3.6': 'Some **Markdown** release _notes_ for 0.3.6', + '0.3.5.2': 'Some **Markdown** release _notes_ for 0.3.5.2', '0.3.5.1': 'Some **Markdown** release _notes_ for 0.3.5.1', '0.3.4.4': 'Some **Markdown** release _notes_ for 0.3.4.4', '0.3.4.3': 'Some **Markdown** release _notes_ for 0.3.4.3', @@ -38,38 +49,56 @@ export module Mock { }, } - export const ReleaseNotes: RR.GetReleaseNotesRes = { - '0.19.2': - 'Contrary to popular belief, Lorem Ipsum is not simply random text.', - '0.19.1': 'release notes for Bitcoin 0.19.1', - '0.19.0': 'release notes for Bitcoin 0.19.0', + export const RegistryInfo: T.RegistryInfo = { + name: 'Start9 Registry', + icon: REGISTRY_ICON, + categories: { + bitcoin: { + name: 'Bitcoin', + description: mockDescription, + }, + featured: { + name: 'Featured', + description: mockDescription, + }, + lightning: { + name: 'Lightning', + description: mockDescription, + }, + communications: { + name: 'Communications', + description: mockDescription, + }, + data: { + name: 'Data', + description: mockDescription, + }, + ai: { + name: 'AI', + description: mockDescription, + }, + }, } - export const MockManifestBitcoind: Manifest = { + export const MockManifestBitcoind: T.Manifest = { id: 'bitcoind', title: 'Bitcoin Core', - version: '0.21.0', - 'git-hash': 'abcdefgh', + version: '0.21.0:0', + satisfies: [], + canMigrateTo: '!', + canMigrateFrom: '*', + gitHash: 'abcdefgh', description: { short: 'A Bitcoin full node by Bitcoin Core.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.', }, - replaces: ['banks', 'governments'], - 'release-notes': 'Taproot, Schnorr, and more.', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, + releaseNotes: 'Taproot, Schnorr, and more.', license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', - 'upstream-repo': 'https://github.com/bitcoin/bitcoin', - 'support-site': 'https://bitcoin.org', - 'marketing-site': 'https://bitcoin.org', - 'donation-url': 'https://start9.com', + wrapperRepo: 'https://github.com/start9labs/bitcoind-wrapper', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + donationUrl: 'https://start9.com', alerts: { install: 'Bitcoin can take over a week to sync.', uninstall: @@ -78,302 +107,43 @@ export module Mock { start: 'Starting Bitcoin is good for your health.', stop: null, }, - main: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': '1ms', - }, - 'health-checks': {}, - config: { - get: null, - set: null, - }, - volumes: {}, - 'min-os-version': '0.2.12', - interfaces: { - ui: { - name: 'Node Visualizer', - description: - 'Web application for viewing information about your node and the Bitcoin network.', - ui: true, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': {}, - protocols: [], - }, - rpc: { - name: 'RPC', - description: 'Used by wallets to interact with your Bitcoin Core node.', - ui: false, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': {}, - protocols: [], - }, - p2p: { - name: 'P2P', - description: - 'Used by other Bitcoin nodes to communicate and interact with your node.', - ui: false, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': {}, - protocols: [], + osVersion: '0.2.12', + dependencies: {}, + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', }, }, - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, + assets: [], + volumes: ['main'], + hardwareRequirements: { + device: [], + arch: null, + ram: null, }, - migrations: null, - actions: { - resync: { - name: 'Resync Blockchain', - description: 'Use this to resync the Bitcoin blockchain from genesis', - warning: 'This will take a couple of days.', - 'allowed-statuses': [ - PackageMainStatus.Running, - PackageMainStatus.Stopped, - ], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': { - reason: { - type: 'string', - name: 'Re-sync Reason', - description: 'Your reason for re-syncing. Why are you doing this?', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - name: { - type: 'string', - name: 'Your Name', - description: 'Tell the class your name.', - nullable: true, - masked: false, - copyable: false, - warning: 'You may loose all your money by providing your name.', - }, - notifications: { - name: 'Notification Preferences', - type: 'list', - subtype: 'enum', - description: 'how you want to be notified', - range: '[1,3]', - default: ['email'], - spec: { - 'value-names': { - email: 'Email', - text: 'Text', - call: 'Call', - push: 'Push', - webhook: 'Webhook', - }, - values: ['email', 'text', 'call', 'push', 'webhook'], - }, - }, - 'days-ago': { - type: 'number', - name: 'Days Ago', - description: 'Number of days to re-sync.', - nullable: false, - default: 100, - range: '[0, 9999]', - integral: true, - }, - 'top-speed': { - type: 'number', - name: 'Top Speed', - description: 'The fastest you can possibly run.', - nullable: false, - range: '[-1000, 1000]', - integral: false, - units: 'm/s', - }, - testnet: { - name: 'Testnet', - type: 'boolean', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: false, - }, - randomEnum: { - name: 'Random Enum', - type: 'enum', - 'value-names': { - null: 'Null', - good: 'Good', - bad: 'Bad', - ugly: 'Ugly', - }, - default: 'null', - description: 'This is not even real.', - warning: 'Be careful changing this!', - values: ['null', 'good', 'bad', 'ugly'], - }, - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - spec: { - name: { - type: 'string', - name: 'Name', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - email: { - type: 'string', - name: 'Email', - nullable: false, - masked: false, - copyable: true, - }, - }, - }, - ips: { - name: 'Whitelist IPs', - type: 'list', - subtype: 'string', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - range: '[1,10]', - default: ['192.168.1.1'], - spec: { - pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$', - 'pattern-description': 'Must be a valid IP address', - masked: false, - copyable: false, - }, - }, - bitcoinNode: { - type: 'union', - default: 'internal', - tag: { - id: 'type', - 'variant-names': { - internal: 'Internal', - external: 'External', - }, - name: 'Bitcoin Node Settings', - description: 'The node settings', - warning: 'Careful changing this', - }, - variants: { - internal: { - 'lan-address': { - name: 'LAN Address', - type: 'pointer', - subtype: 'package', - target: 'lan-address', - description: 'the lan address', - interface: 'tor-address', - 'package-id': '12341234', - }, - 'friendly-name': { - name: 'Friendly Name', - type: 'string', - description: 'the lan address', - nullable: true, - masked: false, - copyable: false, - }, - }, - external: { - 'public-domain': { - name: 'Public Domain', - type: 'string', - description: 'the public address of the node', - nullable: false, - default: 'bitcoinnode.com', - pattern: '.*', - 'pattern-description': 'anything', - masked: false, - copyable: true, - }, - }, - }, - }, - }, - }, - }, - dependencies: {}, } - export const MockManifestLnd: Manifest = { + export const MockManifestLnd: T.Manifest = { id: 'lnd', title: 'Lightning Network Daemon', - version: '0.11.1', + version: '0.11.1:0', + satisfies: [], + canMigrateTo: '!', + canMigrateFrom: '*', + gitHash: 'abcdefgh', description: { short: 'A bolt spec compliant client.', long: 'More info about LND. More info about LND. More info about LND.', }, - 'release-notes': 'Dual funded channels!', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, + releaseNotes: 'Dual funded channels!', license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', - 'upstream-repo': 'https://github.com/lightningnetwork/lnd', - 'support-site': 'https://lightning.engineering/', - 'marketing-site': 'https://lightning.engineering/', - 'donation-url': null, + wrapperRepo: 'https://github.com/start9labs/lnd-wrapper', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/', + marketingSite: 'https://lightning.engineering/', + donationUrl: null, alerts: { install: null, uninstall: null, @@ -382,152 +152,55 @@ export module Mock { start: 'Starting LND is good for your health.', stop: null, }, - main: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': '10000µs', - }, - 'health-checks': {}, - config: { - get: null, - set: null, - }, - volumes: {}, - 'min-os-version': '0.2.12', - interfaces: { - rpc: { - name: 'RPC interface', - description: 'Good for connecting to your node at a distance.', - ui: true, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': { - '44': { - ssl: true, - mapping: 33, - }, - }, - protocols: [], - }, - grpc: { - name: 'GRPC', - description: 'Certain wallet use grpc.', - ui: false, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': { - '66': { - ssl: true, - mapping: 55, - }, - }, - protocols: [], - }, - }, - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - }, - migrations: null, - actions: { - resync: { - name: 'Resync Network Graph', - description: 'Your node will resync its network graph.', - warning: 'This will take a couple hours.', - 'allowed-statuses': [PackageMainStatus.Running], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': null, - }, - }, + osVersion: '0.2.12', dependencies: { bitcoind: { - version: '=0.21.0', description: 'LND needs bitcoin to live.', - requirement: { - type: 'opt-out', - how: 'You can use an external node from your server if you prefer.', - }, - config: null, + optional: true, + s9pk: '', }, 'btc-rpc-proxy': { - version: '>=0.2.2', description: 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', - requirement: { - type: 'opt-in', - how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, - }, - config: null, + optional: true, + s9pk: '', + }, + }, + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', }, }, + assets: [], + volumes: ['main'], + hardwareRequirements: { + device: [], + arch: null, + ram: null, + }, } - export const MockManifestBitcoinProxy: Manifest = { + export const MockManifestBitcoinProxy: T.Manifest = { id: 'btc-rpc-proxy', title: 'Bitcoin Proxy', - version: '0.2.2', - 'git-hash': 'lmnopqrx', + version: '0.2.2:0', + satisfies: [], + canMigrateTo: '!', + canMigrateFrom: '*', + gitHash: 'lmnopqrx', description: { short: 'A super charger for your Bitcoin node.', long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.', }, - 'release-notes': 'Even better support for Bitcoin and wallets!', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, + releaseNotes: 'Even better support for Bitcoin and wallets!', license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper', - 'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy', - 'support-site': '', - 'marketing-site': '', - 'donation-url': 'https://start9.com', + wrapperRepo: 'https://github.com/start9labs/btc-rpc-proxy-wrapper', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: '', + marketingSite: '', + donationUrl: 'https://start9.com', alerts: { install: 'Testing install alert', uninstall: null, @@ -535,251 +208,539 @@ export module Mock { start: null, stop: null, }, - main: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': '1m', - }, - 'health-checks': {}, - config: { get: {} as any, set: {} as any }, - volumes: {}, - 'min-os-version': '0.2.12', - interfaces: { - rpc: { - name: 'RPC interface', - description: 'Good for connecting to your node at a distance.', - ui: false, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': { - 44: { - ssl: true, - mapping: 33, - }, - }, - protocols: [], - }, - }, - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - }, - migrations: null, - actions: {}, + osVersion: '0.2.12', dependencies: { bitcoind: { - version: '>=0.20.0', description: 'Bitcoin Proxy requires a Bitcoin node.', - requirement: { - type: 'required', - }, - config: { - check: { - type: 'docker', - image: 'alpine', - system: true, - entrypoint: 'true', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Cbor, - inject: false, - 'shm-size': '10m', - 'sigterm-timeout': null, - }, - 'auto-configure': { - type: 'docker', - image: 'alpine', - system: true, - entrypoint: 'cat', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Cbor, - inject: false, - 'shm-size': '10m', - 'sigterm-timeout': null, - }, - }, + optional: false, + s9pk: '', + }, + }, + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', }, }, + assets: [], + volumes: ['main'], + hardwareRequirements: { + device: [], + arch: null, + ram: null, + }, } - export const BitcoinDep: DependencyMetadata = { + export const BitcoinDep: T.DependencyMetadata = { title: 'Bitcoin Core', icon: BTC_ICON, - hidden: true, + optional: false, + description: 'Needed to run', } - export const ProxyDep: DependencyMetadata = { + export const ProxyDep: T.DependencyMetadata = { title: 'Bitcoin Proxy', icon: PROXY_ICON, - hidden: false, + optional: true, + description: 'Needed to run', } - export const MarketplacePkgs: { - [id: string]: { - [version: string]: MarketplacePkg - } + export const OtherPackageVersions: { + [id: T.PackageId]: GetPackagesRes } = { bitcoind: { - '0.19.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.19.0', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - 'dependency-metadata': {}, - 'published-at': new Date().toISOString(), - }, - '0.20.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.20.0', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - 'dependency-metadata': {}, - 'published-at': new Date().toISOString(), - }, - '0.21.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.21.0', - 'release-notes': - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - 'dependency-metadata': {}, - 'published-at': new Date().toISOString(), - }, - latest: { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - 'release-notes': - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
Or in [markdown](https://bitcoincore.org/en/releases/0.21.0/)
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - 'dependency-metadata': {}, - 'published-at': new Date().toISOString(), + '=26.1.0:0.1.0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + }, + }, + '=#knots:26.1.20240325:0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + }, }, }, lnd: { - '0.11.0': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.0', - 'release-notes': 'release notes for LND 0.11.0', - }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - 'dependency-metadata': { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, - }, - 'published-at': new Date().toISOString(), - }, - '0.11.1': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.1', - 'release-notes': 'release notes for LND 0.11.1', - }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - 'dependency-metadata': { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, - }, - 'published-at': new Date().toISOString(), - }, - latest: { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestLnd, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - 'dependency-metadata': { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, - }, - 'published-at': new Date(new Date().valueOf() + 10).toISOString(), + '=0.17.5:0': { + best: { + '0.17.5:0': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.5', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['lightning'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, + }, + }, + '=0.17.4-beta:1.0-alpha': { + best: { + '0.17.4-beta:1.0-alpha': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.4', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['lightning'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, + }, }, }, 'btc-rpc-proxy': { - latest: { - icon: PROXY_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestBitcoinProxy, + '=0.3.2.6:0': { + best: { + '0.3.2.6:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, categories: ['bitcoin'], - versions: ['0.2.2'], - 'dependency-metadata': { - bitcoind: BitcoinDep, + otherVersions: { + '0.3.2.7:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, }, - 'published-at': new Date().toISOString(), }, }, } - export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = - Object.values(Mock.MarketplacePkgs).map(service => service['latest']) + export const RegistryPackages: GetPackagesRes = { + bitcoind: { + best: { + '27.0.0:1.0.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:27.1.0:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '26.1.0:0.1.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:26.1.20240325:0': { + releaseNotes: 'Even better Knots support for Bitcoin and wallets!', + }, + }, + }, + lnd: { + best: { + '0.18.0:0.0.1': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: null, + description: 'Used for authorized RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['lightning'], + otherVersions: { + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, + }, + }, + 'btc-rpc-proxy': { + best: { + '0.3.2.7:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: [], ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin'], + otherVersions: { + '0.3.2.6:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, + }, + }, + } export const Notifications: ServerNotifications = [ { id: 1, - 'package-id': null, - 'created-at': '2019-12-26T14:20:30.872Z', + packageId: null, + createdAt: '2019-12-26T14:20:30.872Z', code: 1, level: NotificationLevel.Success, title: 'Backup Complete', @@ -798,9 +759,9 @@ export module Mock { }, { id: 2, - 'package-id': null, - 'created-at': '2019-12-26T14:20:30.872Z', - code: 2, + packageId: null, + createdAt: '2019-12-26T14:20:30.872Z', + code: 0, level: NotificationLevel.Warning, title: 'SSH Key Added', message: 'A new SSH key was added. If you did not do this, shit is bad.', @@ -808,9 +769,9 @@ export module Mock { }, { id: 3, - 'package-id': null, - 'created-at': '2019-12-26T14:20:30.872Z', - code: 3, + packageId: null, + createdAt: '2019-12-26T14:20:30.872Z', + code: 0, level: NotificationLevel.Info, title: 'SSH Key Removed', message: 'A SSH key was removed.', @@ -818,9 +779,9 @@ export module Mock { }, { id: 4, - 'package-id': 'bitcoind', - 'created-at': '2019-12-26T14:20:30.872Z', - code: 4, + packageId: 'bitcoind', + createdAt: '2019-12-26T14:20:30.872Z', + code: 0, level: NotificationLevel.Error, title: 'Service Crashed', message: new Array(40) @@ -833,6 +794,16 @@ export module Mock { .join(''), data: null, }, + { + id: 5, + packageId: null, + createdAt: '2019-12-26T14:20:30.872Z', + code: 2, + level: NotificationLevel.Success, + title: 'Welcome to StartOS 0.3.6!', + message: 'Click "View Details" to learn all about the new version', + data: markdown, + }, ] export function getServerMetrics() { @@ -844,7 +815,7 @@ export module Mock { }, }, memory: { - 'percentage-used': { + percentageUsed: { value: '30.7', unit: '%', }, @@ -860,29 +831,29 @@ export module Mock { value: '8784.97', unit: 'MiB', }, - 'zram-total': { + zramTotal: { value: '7992.00', unit: 'MiB', }, - 'zram-available': { + zramAvailable: { value: '7882.50', unit: 'MiB', }, - 'zram-used': { + zramUsed: { value: '109.50', unit: 'MiB', }, }, cpu: { - 'percentage-used': { + percentageUsed: { value: '8.4', unit: '%', }, - 'user-space': { + userSpace: { value: '7.0', unit: '%', }, - 'kernel-space': { + kernelSpace: { value: '1.4', unit: '%', }, @@ -908,7 +879,7 @@ export module Mock { value: '992.59', unit: 'GB', }, - 'percentage-used': { + percentageUsed: { value: '46.4', unit: '%', }, @@ -916,53 +887,27 @@ export module Mock { } } - export function getAppMetrics() { - const metr: Metric = { - Metric1: { - value: Math.random(), - unit: 'mi/b', - }, - Metric2: { - value: Math.random(), - unit: '%', - }, - Metric3: { - value: 10.1, - unit: '%', - }, - } - - return metr - } - export const ServerLogs: Log[] = [ { timestamp: '2022-07-28T03:52:54.808769Z', message: '****** START *****', + bootId: 'hsjnfdklasndhjasvbjamsksajbndjn', }, { timestamp: '2019-12-26T14:21:30.872Z', message: '\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.startos/api/graphql \u001b[0;36;49m1.169406ms\u001b', + bootId: 'hsjnfdklasndhjasvbjamsksajbndjn', }, { timestamp: '2019-12-26T14:22:30.872Z', message: '****** FINISH *****', - }, - ] - - export const PackageLogs: Log[] = [ - { - timestamp: '2022-07-28T03:52:54.808769Z', - message: '****** START *****', - }, - { - timestamp: '2019-12-26T14:21:30.872Z', - message: 'PackageLogs PackageLogs PackageLogs PackageLogs PackageLogs', + bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm', }, { - timestamp: '2019-12-26T14:22:30.872Z', - message: '****** FINISH *****', + timestamp: '2019-12-26T15:22:30.872Z', + message: '****** AGAIN *****', + bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm', }, ] @@ -970,15 +915,17 @@ export module Mock { current: 'b7b1a9cef4284f00af9e9dda6e676177', sessions: { '9513226517c54ddd8107d6d7b9d8aed7': { - 'last-active': '2021-07-14T20:49:17.774Z', - 'user-agent': 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', + loggedIn: '2021-07-14T20:49:17.774Z', + lastActive: '2021-07-14T20:49:17.774Z', + userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', metadata: { platforms: ['iphone', 'mobileweb', 'mobile', 'ios'], }, }, b7b1a9cef4284f00af9e9dda6e676177: { - 'last-active': '2021-06-14T20:49:17.774Z', - 'user-agent': + loggedIn: '2021-07-14T20:49:17.774Z', + lastActive: '2021-06-14T20:49:17.774Z', + userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', metadata: { platforms: ['desktop'], @@ -987,23 +934,15 @@ export module Mock { }, } - export const ActionResponse: RR.ExecutePackageActionRes = { - message: - 'Password changed successfully. If you lose your new password, you will be lost forever.', - value: 'NewPassword1234!', - copyable: true, - qr: true, - } - export const SshKeys: RR.GetSSHKeysRes = [ { - 'created-at': new Date().toISOString(), + createdAt: new Date().toISOString(), alg: 'ed25519', hostname: 'Matt Key', fingerprint: '28:d2:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53', }, { - 'created-at': new Date().toISOString(), + createdAt: new Date().toISOString(), alg: 'ed25519', hostname: 'Aiden Key', fingerprint: '12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53', @@ -1011,7 +950,7 @@ export module Mock { ] export const SshKey: RR.AddSSHKeyRes = { - 'created-at': new Date().toISOString(), + createdAt: new Date().toISOString(), alg: 'ed25519', hostname: 'Lucy Key', fingerprint: '44:44:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53', @@ -1025,7 +964,7 @@ export module Mock { }, connected: 'Goosers', country: 'US', - 'available-wifi': [ + availableWifi: [ { ssid: 'Goosers a billion', strength: 40, @@ -1051,13 +990,16 @@ export module Mock { path: '/Desktop/startos-backups', username: 'TestUser', mountable: false, - 'embassy-os': { - version: '0.3.0', - full: true, - 'password-hash': - // password is asdfasdf - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': '', + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.3.6', + passwordHash: + // password is asdfasdf + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, }, }, // 'ftcvewdnkemfksdm': { @@ -1068,7 +1010,7 @@ export module Mock { // used: 0, // model: 'Evo SATA 2.5', // vendor: 'Samsung', - // 'embassy-os': null, + // startOs: {}, // }, csgashbdjkasnd: { type: 'cifs', @@ -1076,7 +1018,7 @@ export module Mock { path: '/Desktop/startos-backups-2', username: 'TestUser', mountable: true, - 'embassy-os': null, + startOs: {}, }, powjefhjbnwhdva: { type: 'disk', @@ -1086,744 +1028,648 @@ export module Mock { used: 100000000000, model: null, vendor: 'SSK', - 'embassy-os': { - version: '0.3.0', - full: true, - // password is asdfasdf - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': '', + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.3.6', + passwordHash: + // password is asdfasdf + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, }, }, } export const BackupInfo: RR.GetBackupInfoRes = { - version: '0.3.0', + version: '0.3.6', timestamp: new Date().toISOString(), - 'package-backups': { + packageBackups: { bitcoind: { title: 'Bitcoin Core', - version: '0.21.0', - 'os-version': '0.3.0', + version: '0.21.0:0', + osVersion: '0.3.6', timestamp: new Date().toISOString(), }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', - version: '0.2.2', - 'os-version': '0.3.0', + version: '0.2.2:0', + osVersion: '0.3.6', timestamp: new Date().toISOString(), }, }, } - export const PackageProperties: RR.GetPackagePropertiesRes<2> = { - version: 2, - data: { - lndconnect: { - type: 'string', - description: 'This is some information about the thing.', - copyable: true, - qr: true, - masked: true, - value: - 'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA', - }, - Nested: { - type: 'object', - description: 'This is a nested thing metric', - value: { - 'Last Name': { - type: 'string', - description: 'The last name of the user', - copyable: true, - qr: true, - masked: false, - value: 'Hill', - }, - Age: { - type: 'string', - description: 'The age of the user', - copyable: false, - qr: false, - masked: false, - value: '35', - }, - Password: { - type: 'string', - description: 'A secret password', - copyable: true, - qr: false, - masked: true, - value: 'password123', - }, - }, - }, - 'Another Value': { - type: 'string', - description: 'Some more information about the service.', - copyable: false, - qr: true, - masked: false, - value: 'https://guessagain.com', - }, + export const ActionResMessage: RR.ActionRes = { + version: '1', + title: 'New Password', + message: + 'Action was run successfully and smoothly and fully and all is good on the western front.', + result: null, + } + + export const ActionResSingle: RR.ActionRes = { + version: '1', + title: 'New Password', + message: + 'Action was run successfully and smoothly and fully and all is good on the western front.', + result: { + type: 'single', + copyable: true, + qr: true, + masked: true, + value: 'iwejdoiewdhbew', }, - } as any // @TODO why is this necessary? + } - export const ConfigSpec: RR.GetPackageConfigRes['spec'] = { - bitcoin: { - type: 'object', - name: 'Bitcoin Settings', - description: - 'RPC and P2P interface configuration options for Bitcoin Core', - spec: { - 'bitcoind-p2p': { - type: 'union', - tag: { - id: 'type', - name: 'Bitcoin Core P2P', - description: - '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', - 'variant-names': { - internal: 'Bitcoin Core', - external: 'External Node', - }, - }, - default: 'internal', - variants: { - internal: {}, - external: { - 'p2p-host': { - type: 'string', - name: 'Public Address', - description: 'The public address of your Bitcoin Core server', - nullable: false, - masked: false, - copyable: false, - }, - 'p2p-port': { - type: 'number', - name: 'P2P Port', - description: - 'The port that your Bitcoin Core P2P server is bound to', - nullable: false, - range: '[0,65535]', - integral: true, - default: 8333, - }, - }, - }, + export const ActionResGroup: RR.ActionRes = { + version: '1', + title: 'Properties', + message: + 'Successfully retrieved properties. Here is a bunch of useful information about this service.', + result: { + type: 'group', + value: [ + { + type: 'single', + name: 'LND Connect', + description: 'This is some information about the thing.', + copyable: true, + qr: true, + masked: true, + value: + 'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA', }, - }, - }, - advanced: { - name: 'Advanced', - type: 'object', - description: 'Advanced settings', - spec: { - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - rpcuser2: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, + { + type: 'group', + name: 'Nested Stuff', + description: 'This is a nested thing metric', + value: [ + { + type: 'single', + name: 'Last Name', + description: 'The last name of the user', copyable: true, + qr: true, + masked: false, + value: 'Hill', }, - rpcuser: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', + { + type: 'single', + name: 'Age', + description: 'The age of the user', + copyable: false, + qr: false, masked: false, - copyable: true, + value: '35', }, - rpcpass: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, + { + type: 'single', + name: 'Password', + description: 'A secret password', copyable: true, - }, - rpcpass2: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, + qr: false, masked: true, - copyable: true, + value: 'password123', }, - }, - }, - }, - }, - testnet: { - name: 'Testnet', - type: 'boolean', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: true, - }, - 'object-list': { - name: 'Object List', - type: 'list', - subtype: 'object', - description: 'This is a list of objects, like users or something', - range: '[0,4]', - default: [ - { - 'first-name': 'Admin', - 'last-name': 'User', - age: 40, + ], }, { - 'first-name': 'Admin2', - 'last-name': 'User', - age: 40, + type: 'single', + name: 'Another Value', + description: 'Some more information about the service.', + copyable: false, + qr: true, + masked: false, + value: 'https://guessagain.com', }, ], - // the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements. - // it just so happens that ValueSpecObject's have the field { spec: ConfigSpec } - // see 'union-list' below for a different example. - spec: { - 'unique-by': 'last-name', - 'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`, - spec: { - 'first-name': { - name: 'First Name', - type: 'string', - description: 'User first name', - nullable: true, - masked: false, - copyable: false, - }, - 'last-name': { - name: 'Last Name', - type: 'string', - description: 'User first name', - nullable: true, - default: { - charset: 'a-g,2-9', - len: 12, - }, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - age: { - name: 'Age', - type: 'number', - description: 'The age of the user', - nullable: true, - integral: false, - warning: 'User must be at least 18.', - range: '[18,*)', - }, - }, - }, }, - 'union-list': { - name: 'Union List', - type: 'list', - subtype: 'union', - description: 'This is a sample list of unions', - warning: 'If you change this, things may work.', - // a list of union selections. e.g. 'summer', 'winter',... - default: ['summer'], - range: '[0, 2]', - spec: { - tag: { - id: 'preference', - 'variant-names': { - summer: 'Summer', - winter: 'Winter', - other: 'Other', + } + + export const getActionInputSpec = async (): Promise => + configBuilderToSpec( + ISB.InputSpec.of({ + bitcoin: ISB.Value.object( + { + name: 'Bitcoin Settings', + description: + 'RPC and P2P interface configuration options for Bitcoin Core', }, - name: 'Preference', - }, - // this default is used to make a union selection when a new list element is first created - default: 'summer', - variants: { - summer: { - 'favorite-tree': { - name: 'Favorite Tree', - type: 'string', - nullable: false, - description: 'What is your favorite tree?', - default: 'Maple', - masked: false, - copyable: false, - }, - 'favorite-flower': { - name: 'Favorite Flower', - type: 'enum', - description: 'Select your favorite flower', - 'value-names': { - none: 'Hate Flowers', - red: 'Red', - blue: 'Blue', - purple: 'Purple', + ISB.InputSpec.of({ + 'bitcoind-p2p': ISB.Value.union( + { + name: 'P2P Settings', + description: + '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', + default: 'internal', }, - values: ['none', 'red', 'blue', 'purple'], - default: 'none', - }, + ISB.Variants.of({ + internal: { name: 'Bitcoin Core', spec: ISB.InputSpec.of({}) }, + external: { + name: 'External Node', + spec: ISB.InputSpec.of({ + 'p2p-host': ISB.Value.text({ + name: 'Public Address', + required: false, + default: null, + description: + 'The public address of your Bitcoin Core server', + }), + 'p2p-port': ISB.Value.number({ + name: 'P2P Port', + description: + 'The port that your Bitcoin Core P2P server is bound to', + required: true, + default: 8333, + min: 0, + max: 65535, + integer: true, + }), + }), + }, + }), + ), + }), + ), + color: ISB.Value.color({ + name: 'Color', + required: false, + default: null, + }), + datetime: ISB.Value.datetime({ + name: 'Datetime', + required: false, + default: null, + }), + // file: ISB.Value.file({ + // name: 'File', + // required: false, + // extensions: ['png', 'pdf'], + // }), + users: ISB.Value.multiselect({ + name: 'Users', + default: [], + maxLength: 2, + values: { + matt: 'Matt Hill', + alex: 'Alex Inkin', + blue: 'Blue J', + lucy: 'Lucy', }, - winter: { - 'like-snow': { - name: 'Like Snow?', - type: 'boolean', - description: 'Do you like snow or not?', - default: true, - }, + }), + advanced: ISB.Value.object( + { + name: 'Advanced', + description: 'Advanced settings', }, - }, - 'unique-by': 'preference', - }, - }, - 'random-enum': { - name: 'Random Enum', - type: 'enum', - 'value-names': { - null: 'Null', - option1: 'One 1', - option2: 'Two 2', - option3: 'Three 3', - }, - default: 'null', - description: 'This is not even real.', - warning: 'Be careful changing this!', - values: ['null', 'option1', 'option2', 'option3'], - }, - 'favorite-number': { - name: 'Favorite Number', - type: 'number', - integral: false, - description: 'Your favorite number of all time', - warning: - 'Once you set this number, it can never be changed without severe consequences.', - nullable: true, - default: 7, - range: '(-100,100]', - units: 'BTC', - }, - 'unlucky-numbers': { - name: 'Unlucky Numbers', - type: 'list', - subtype: 'number', - description: 'Numbers that you like but are not your top favorite.', - spec: { - integral: false, - range: '[-100,200)', - }, - range: '[0,10]', - default: [2, 3], - }, - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - spec: { - law1: { - name: 'First Law', - type: 'string', - description: 'the first law', - nullable: true, - masked: false, - copyable: true, + ISB.InputSpec.of({ + rpcsettings: ISB.Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', + }, + ISB.InputSpec.of({ + rpcuser2: ISB.Value.text({ + name: 'RPC Username', + required: false, + default: 'defaultrpcusername', + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcuser: ISB.Value.text({ + name: 'RPC Username', + required: true, + default: 'defaultrpcusername', + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcpass: ISB.Value.text({ + name: 'RPC User Password', + required: true, + default: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, + description: 'rpc password', + }), + rpcpass2: ISB.Value.text({ + name: 'RPC User Password', + required: true, + default: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, + description: 'rpc password', + }), + }), + ), + }), + ), + testnet: ISB.Value.toggle({ + name: 'Testnet', + default: true, + description: + '
  • determines whether your node is running on testnet or mainnet
', + warning: 'Chain will have to resync!', + }), + 'object-list': ISB.Value.list( + ISB.List.obj( + { + name: 'Object List', + minLength: 0, + maxLength: 4, + default: [ + // { 'first-name': 'Admin', 'last-name': 'User', age: 40 }, + // { 'first-name': 'Admin2', 'last-name': 'User', age: 40 }, + ], + description: 'This is a list of objects, like users or something', }, - law2: { - name: 'Second Law', - type: 'string', - description: 'the second law', - nullable: true, - masked: false, - copyable: true, + { + spec: ISB.InputSpec.of({ + 'first-name': ISB.Value.text({ + name: 'First Name', + required: false, + description: 'User first name', + default: 'Matt', + }), + 'last-name': ISB.Value.text({ + name: 'Last Name', + required: true, + default: { + charset: 'a-g,2-9', + len: 12, + }, + description: 'User first name', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + age: ISB.Value.number({ + name: 'Age', + description: 'The age of the user', + warning: 'User must be at least 18.', + required: false, + default: null, + min: 18, + integer: false, + }), + }), + displayAs: `I'm {{last-name}}, {{first-name}} {{last-name}}`, + uniqueBy: 'last-name', }, - }, - }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - subtype: 'object', - description: 'the people who make the rules', - range: '[0,2]', - default: [], - spec: { - 'unique-by': null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'string', - description: 'the name of the rule maker', - nullable: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - copyable: false, - }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'string', - description: 'the ip of the rule maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': 'may only contain numbers and periods', - masked: false, - copyable: true, - }, + ), + ), + 'union-list': ISB.Value.list( + ISB.List.obj( + { + name: 'Union List', + minLength: 0, + maxLength: 2, + default: [], + description: 'This is a sample list of unions', + warning: 'If you change this, things may work.', }, - }, - }, - rpcuser: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcpass: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - copyable: true, - }, - }, - }, - 'bitcoin-node': { - type: 'union', - default: 'internal', - tag: { - id: 'type', - 'variant-names': { - internal: 'Internal', - external: 'External', - }, - name: 'Bitcoin Node Settings', - description: 'Options
  • Item 1
  • Item 2
', - warning: 'Careful changing this', - }, - variants: { - internal: { - 'lan-address': { - name: 'LAN Address', - type: 'pointer', - subtype: 'package', - target: 'lan-address', - 'package-id': 'bitcoind', - description: 'the lan address', - interface: 'asdf', - }, - }, - external: { - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - spec: { - name: { - type: 'string', - name: 'Name', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - email: { - type: 'string', - name: 'Email', - nullable: false, - masked: false, - copyable: true, - }, + { + spec: ISB.InputSpec.of({ + /* TODO: Convert range for this value ([0, 2])*/ + union: ISB.Value.union( + { + name: 'Preference', + description: null, + warning: null, + default: 'summer', + }, + ISB.Variants.of({ + summer: { + name: 'summer', + spec: ISB.InputSpec.of({ + 'favorite-tree': ISB.Value.text({ + name: 'Favorite Tree', + required: true, + default: 'Maple', + description: 'What is your favorite tree?', + }), + 'favorite-flower': ISB.Value.select({ + name: 'Favorite Flower', + description: 'Select your favorite flower', + default: 'none', + values: { + none: 'none', + red: 'red', + blue: 'blue', + purple: 'purple', + }, + }), + }), + }, + winter: { + name: 'winter', + spec: ISB.InputSpec.of({ + 'like-snow': ISB.Value.toggle({ + name: 'Like Snow?', + default: true, + description: 'Do you like snow or not?', + }), + }), + }, + }), + ), + }), + uniqueBy: 'preference', }, + ), + ), + 'random-select': ISB.Value.dynamicSelect(() => ({ + name: 'Random select', + description: 'This is not even real.', + warning: 'Be careful changing this!', + default: 'option1', + values: { + option1: 'option1', + option2: 'option2', + option3: 'option3', }, - 'public-domain': { - name: 'Public Domain', - type: 'string', - description: 'the public address of the node', - nullable: false, - default: 'bitcoinnode.com', - pattern: '.*', - 'pattern-description': 'anything', - masked: false, - copyable: true, - }, - 'private-domain': { - name: 'Private Domain', - type: 'string', - description: 'the private address of the node', - nullable: false, - masked: true, - copyable: true, - }, - }, - }, - }, - port: { - name: 'Port', - type: 'number', - integral: true, - description: - 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', - nullable: false, - default: 8333, - range: '(0, 9998]', - }, - 'favorite-slogan': { - name: 'Favorite Slogan', - type: 'string', - description: - 'You most favorite slogan in the whole world, used for paying you.', - nullable: true, - masked: true, - copyable: true, - }, - rpcallowip: { - name: 'RPC Allowed IPs', - type: 'list', - subtype: 'string', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - range: '[1,10]', - default: ['192.168.1.1'], - spec: { - masked: false, - copyable: false, - pattern: - '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', - 'pattern-description': 'must be a valid ipv4, ipv6, or domain name', - }, - }, - rpcauth: { - name: 'RPC Auth', - type: 'list', - subtype: 'string', - description: 'api keys that are authorized to access your Bitcoin node.', - range: '[0,*)', - default: [], - spec: { - masked: false, - copyable: false, - }, - }, - 'more-advanced': { - name: 'More Advanced', - type: 'object', - description: 'Advanced settings', - spec: { - notifications: { - name: 'Notification Preferences', - type: 'list', - subtype: 'enum', - description: 'how you want to be notified', - range: '[1,3]', - default: ['email'], - spec: { - 'value-names': { - email: 'EEEEmail', - text: 'Texxxt', - call: 'Ccccall', - push: 'PuuuusH', - webhook: 'WebHooookkeee', + disabled: ['option2'], + })), + 'favorite-number': + /* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number( + { + name: 'Favorite Number', + description: 'Your favorite number of all time', + warning: + 'Once you set this number, it can never be changed without severe consequences.', + required: false, + default: 7, + integer: false, + units: 'BTC', }, - values: ['email', 'text', 'call', 'push', 'webhook'], + ), + rpcsettings: ISB.Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', }, - }, - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - spec: { - law1: { + ISB.InputSpec.of({ + laws: ISB.Value.object( + { + name: 'Laws', + description: 'the law of the realm', + }, + ISB.InputSpec.of({ + law1: ISB.Value.text({ name: 'First Law', - type: 'string', + required: false, description: 'the first law', - nullable: true, - masked: false, - copyable: true, - }, - law2: { + default: null, + }), + law2: ISB.Value.text({ name: 'Second Law', - type: 'string', + required: false, description: 'the second law', - nullable: true, - masked: false, - copyable: true, - }, - law4: { - name: 'Fourth Law', - type: 'string', - description: 'the fourth law', - nullable: true, - masked: false, - copyable: true, + default: null, + }), + }), + ), + rulemakers: ISB.Value.list( + ISB.List.obj( + { + name: 'Rule Makers', + minLength: 0, + maxLength: 2, + description: 'the people who make the rules', }, - law3: { - name: 'Third Law', - type: 'list', - subtype: 'object', - description: 'the third law', - range: '[0,2]', - default: [], - spec: { - 'unique-by': null, - spec: { - lawname: { - name: 'Law Name', - type: 'string', - description: 'the name of the law maker', - nullable: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - copyable: false, - }, - lawagency: { - name: 'Law agency', - type: 'string', - description: 'the ip of the law maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': - 'may only contain numbers and periods', - masked: false, - copyable: true, + { + spec: ISB.InputSpec.of({ + rulemakername: ISB.Value.text({ + name: 'Rulemaker Name', + required: true, + default: { + charset: 'a-g,2-9', + len: 12, }, - }, - }, - }, - law5: { - name: 'Fifth Law', - type: 'string', - description: 'the fifth law', - nullable: true, - masked: false, - copyable: true, - }, - }, - }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - subtype: 'object', - description: 'the people who make the rules', - range: '[0,2]', - default: [], - spec: { - 'unique-by': null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'string', - description: 'the name of the rule maker', - nullable: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - copyable: false, - }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'string', - description: 'the ip of the rule maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': - 'may only contain numbers and periods', - masked: false, - copyable: true, - }, + description: 'the name of the rule maker', + }), + rulemakerip: ISB.Value.text({ + name: 'Rulemaker IP', + required: true, + default: '192.168.1.0', + description: 'the ip of the rule maker', + patterns: [ + { + regex: + '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', + description: 'may only contain numbers and periods', + }, + ], + }), + }), }, - }, - }, - rpcuser: { + ), + ), + rpcuser: ISB.Value.text({ name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, + required: true, default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcpass: { + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcpass: ISB.Value.text({ name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, + required: true, default: { charset: 'a-z,A-Z,2-9', len: 20, }, + description: 'rpc password', masked: true, - copyable: true, + }), + }), + ), + 'bitcoin-node': ISB.Value.union( + { + name: 'Bitcoin Node', + description: 'Options
  • Item 1
  • Item 2
', + warning: 'Careful changing this', + default: 'internal', + }, + ISB.Variants.of({ + fake: { + name: 'Fake', + spec: ISB.InputSpec.of({}), + }, + internal: { + name: 'Internal', + spec: ISB.InputSpec.of({ + listitems: ISB.Value.list( + ISB.List.text( + { + name: 'RPC Allowed IPs', + minLength: 1, + maxLength: 10, + default: ['192.168.1.1'], + description: + 'external ip addresses that are authorized to access your Bitcoin node', + warning: + 'Any IP you allow here will have RPC access to your Bitcoin node.', + }, + { + patterns: [ + { + regex: + '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', + description: + 'must be a valid ipv4, ipv6, or domain name', + }, + ], + }, + ), + ), + name: ISB.Value.text({ + name: 'Name', + required: false, + default: null, + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'Must contain only letters.', + }, + ], + }), + }), + }, + external: { + name: 'External', + spec: ISB.InputSpec.of({ + 'emergency-contact': ISB.Value.object( + { + name: 'Emergency Contact', + description: 'The person to contact in case of emergency.', + }, + ISB.InputSpec.of({ + name: ISB.Value.text({ + name: 'Name', + required: false, + default: null, + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'Must contain only letters.', + }, + ], + }), + email: ISB.Value.text({ + name: 'Email', + inputmode: 'email', + required: false, + default: null, + }), + }), + ), + 'public-domain': ISB.Value.text({ + name: 'Public Domain', + required: true, + default: 'bitcoinnode.com', + description: 'the public address of the node', + patterns: [ + { + regex: '.*', + description: 'anything', + }, + ], + }), + 'private-domain': ISB.Value.text({ + name: 'Private Domain', + required: false, + default: null, + description: 'the private address of the node', + masked: true, + inputmode: 'url', + }), + }), }, + }), + ), + port: ISB.Value.number({ + name: 'Port', + description: + 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', + required: true, + default: 8333, + min: 1, + max: 9998, + step: 1, + integer: true, + }), + 'favorite-slogan': ISB.Value.text({ + name: 'Favorite Slogan', + generate: { + charset: 'a-z,A-Z,2-9', + len: 20, }, - }, - }, - }, - } + required: false, + default: null, + description: + 'You most favorite slogan in the whole world, used for paying you.', + masked: true, + }), + rpcallowip: ISB.Value.list( + ISB.List.text( + { + name: 'RPC Allowed IPs', + minLength: 1, + maxLength: 10, + default: ['192.168.1.1'], + description: + 'external ip addresses that are authorized to access your Bitcoin node', + warning: + 'Any IP you allow here will have RPC access to your Bitcoin node.', + }, + { + patterns: [ + { + regex: + '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', + description: 'must be a valid ipv4, ipv6, or domain name', + }, + ], + }, + ), + ), + rpcauth: ISB.Value.list( + ISB.List.text( + { + name: 'RPC Auth', + minLength: 3, + description: + 'api keys that are authorized to access your Bitcoin node.', + }, + { + patterns: [], + }, + ), + ), + }), + ) export const MockConfig = { testnet: undefined, @@ -1844,8 +1690,7 @@ export module Mock { age: 60, }, ], - 'union-list': undefined, - 'random-enum': 'option2', + 'random-select': ['goodbye'], 'favorite-number': 0, rpcsettings: { laws: { @@ -1857,7 +1702,11 @@ export module Mock { rulemakers: [], }, 'bitcoin-node': { - type: 'internal', + selection: 'internal', + value: { + listitems: ['192.168.1.1', '192.1681.23'], + name: 'Matt', + }, }, port: 20, rpcallowip: undefined, @@ -1867,165 +1716,484 @@ export module Mock { export const MockDependencyConfig = MockConfig - export const bitcoind: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/bitcoind/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/bitcoind.svg', - instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md', - }, - manifest: MockManifestBitcoind, - installed: { + export const bitcoind: PackageDataEntry = { + stateInfo: { + state: 'installed', manifest: MockManifestBitcoind, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: new Date().toISOString(), - health: {}, + }, + dataVersion: MockManifestBitcoind.version, + icon: '/assets/img/service-icons/bitcoind.svg', + lastBackup: null, + status: { + main: 'running', + started: new Date().toISOString(), + health: {}, + }, + actions: { + config: { + name: 'Set Config', + description: 'edit bitcoin.conf', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + rpc: { + name: 'Set RPC', + description: 'Create RPC Credentials', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + properties: { + name: 'View Properties', + description: 'view important information about Bitcoin', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: false, + group: null, + }, + test: { + name: 'Do Another Thing', + description: + 'An example of an action that shows a warning and takes no input', + warning: 'careful running this action', + visibility: { disabled: 'This is temporarily disabled' }, + allowedStatuses: 'only-running', + hasInput: false, + group: null, + }, + }, + serviceInterfaces: { + ui: { + id: 'ui', + masked: false, + name: 'Web UI', + description: + 'A launchable web app for you to interact with your Bitcoin node', + type: 'ui', + addressInfo: { + username: null, + hostId: 'abcdefg', + internalPort: 80, + scheme: 'http', + sslScheme: 'https', + suffix: '', }, - 'dependency-config-errors': {}, }, - 'interface-addresses': { - ui: { - 'tor-address': 'bitcoind-ui-address.onion', - 'lan-address': 'bitcoind-ui-address.local', + rpc: { + id: 'rpc', + masked: false, + name: 'RPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'bcdefgh', + internalPort: 8332, + scheme: 'http', + sslScheme: 'https', + suffix: '', }, - rpc: { - 'tor-address': 'bitcoind-rpc-address.onion', - 'lan-address': 'bitcoind-rpc-address.local', + }, + p2p: { + id: 'p2p', + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'cdefghi', + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, + suffix: '', + }, + }, + }, + currentDependencies: {}, + hosts: { + abcdefg: { + bindings: { + 80: { + enabled: true, + net: { + assignedPort: 80, + assignedSslPort: 443, + public: false, + }, + options: { + addSsl: null, + preferredExternalPort: 443, + secure: { ssl: true }, + }, + }, }, - p2p: { - 'tor-address': 'bitcoind-p2p-address.onion', - 'lan-address': 'bitcoind-p2p-address.local', + onions: [], + domains: {}, + hostnameInfo: { + 80: [ + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.10.11', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv6', + value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]', + scopeId: 2, + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]', + scopeId: 3, + port: null, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p.onion', + port: 80, + sslPort: 443, + }, + }, + ], }, }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], + bcdefgh: { + bindings: { + 8332: { + enabled: true, + net: { + assignedPort: 8332, + assignedSslPort: null, + public: false, + }, + options: { + addSsl: null, + preferredExternalPort: 8332, + secure: { ssl: false }, + }, + }, }, - }, - 'current-dependencies': {}, - 'dependency-info': {}, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', - }, - 'install-progress': undefined, - } - - export const bitcoinProxy: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/btc-rpc-proxy/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/btc-rpc-proxy.png', - instructions: '/public/package-data/btc-rpc-proxy/0.20.0/INSTRUCTIONS.md', - }, - manifest: MockManifestBitcoinProxy, - installed: { - 'last-backup': null, - status: { - configured: false, - main: { - status: PackageMainStatus.Stopped, + onions: [], + domains: {}, + hostnameInfo: { + 8332: [], }, - 'dependency-config-errors': {}, }, - manifest: MockManifestBitcoinProxy, - 'interface-addresses': { - rpc: { - 'tor-address': 'bitcoinproxy-rpc-address.onion', - 'lan-address': 'bitcoinproxy-rpc-address.local', + cdefghi: { + bindings: { + 8333: { + enabled: true, + net: { + assignedPort: 8333, + assignedSslPort: null, + public: false, + }, + options: { + addSsl: null, + preferredExternalPort: 8333, + secure: { ssl: false }, + }, + }, }, - }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], + onions: [], + domains: {}, + hostnameInfo: { + 8333: [], }, }, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], + }, + storeExposedDependents: [], + registry: 'https://registry.start9.com/', + developerKey: 'developer-key', + requestedActions: { + 'bitcoind-config': { + request: { + packageId: 'bitcoind', + actionId: 'config', + severity: 'critical', + reason: + 'You must run Config before starting Bitcoin for the first time', }, + active: true, }, - 'dependency-info': { - bitcoind: { - title: Mock.MockManifestBitcoind.title, - icon: 'assets/img/service-icons/bitcoind.svg', + 'bitcoind-properties': { + request: { + packageId: 'bitcoind', + actionId: 'properties', + severity: 'important', + reason: 'Check out all the info about your Bitcoin node', }, + active: true, }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, - 'install-progress': undefined, } - export const lnd: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/lnd/0.11.0/LICENSE.md', - icon: '/assets/img/service-icons/lnd.png', - instructions: '/public/package-data/lnd/0.11.0/INSTRUCTIONS.md', + export const bitcoinProxy: PackageDataEntry = { + stateInfo: { + state: 'installed', + manifest: MockManifestBitcoinProxy, }, - manifest: MockManifestLnd, - installed: { - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-config-errors': { - 'btc-rpc-proxy': 'Username not found', + dataVersion: MockManifestBitcoinProxy.version, + icon: '/assets/img/service-icons/btc-rpc-proxy.png', + lastBackup: null, + status: { + main: 'stopped', + }, + actions: {}, + serviceInterfaces: { + ui: { + id: 'ui', + masked: false, + name: 'Web UI', + description: 'A launchable web app for Bitcoin Proxy', + type: 'ui', + addressInfo: { + username: null, + hostId: 'hijklmnop', + internalPort: 80, + scheme: 'http', + sslScheme: 'https', + suffix: '', }, }, + }, + currentDependencies: { + bitcoind: { + title: Mock.MockManifestBitcoind.title, + icon: 'assets/img/service-icons/bitcoind.svg', + kind: 'running', + versionRange: '>=26.0.0', + healthChecks: [], + }, + }, + hosts: {}, + storeExposedDependents: [], + registry: 'https://registry.start9.com/', + developerKey: 'developer-key', + requestedActions: {}, + } + + export const lnd: PackageDataEntry = { + stateInfo: { + state: 'installed', manifest: MockManifestLnd, - 'interface-addresses': { - rpc: { - 'tor-address': 'lnd-rpc-address.onion', - 'lan-address': 'lnd-rpc-address.local', - }, - grpc: { - 'tor-address': 'lnd-grpc-address.onion', - 'lan-address': 'lnd-grpc-address.local', + }, + dataVersion: MockManifestLnd.version, + icon: '/assets/img/service-icons/lnd.png', + lastBackup: null, + status: { + main: 'stopped', + }, + actions: { + config: { + name: 'Config', + description: 'LND needs configuration before starting', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + connect: { + name: 'Connect', + description: 'View LND connection details', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + }, + serviceInterfaces: { + grpc: { + id: 'grpc', + masked: false, + name: 'GRPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + internalPort: 10009, + scheme: null, + sslScheme: 'grpc', + suffix: '', }, }, - 'system-pointers': [], - 'current-dependents': {}, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], + lndconnect: { + id: 'lndconnect', + masked: true, + name: 'LND Connect', + description: + 'Used by client wallets adhering to LND Connect protocol to connect to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + internalPort: 10009, + scheme: null, + sslScheme: 'lndconnect', + suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, - 'btc-rpc-proxy': { - pointers: [], - 'health-checks': [], + }, + p2p: { + id: 'p2p', + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'rstuvw', + internalPort: 9735, + scheme: 'lightning', + sslScheme: null, + suffix: '', }, }, - 'dependency-info': { - bitcoind: { - title: Mock.MockManifestBitcoind.title, - icon: 'assets/img/service-icons/bitcoind.svg', + }, + currentDependencies: { + bitcoind: { + title: Mock.MockManifestBitcoind.title, + icon: 'assets/img/service-icons/bitcoind.svg', + kind: 'running', + versionRange: '>=26.0.0', + healthChecks: [], + }, + 'btc-rpc-proxy': { + title: Mock.MockManifestBitcoinProxy.title, + icon: 'assets/img/service-icons/btc-rpc-proxy.png', + kind: 'exists', + versionRange: '>2.0.0', + }, + }, + hosts: {}, + storeExposedDependents: [], + registry: 'https://registry.start9.com/', + developerKey: 'developer-key', + requestedActions: { + config: { + active: true, + request: { + packageId: 'lnd', + actionId: 'config', + severity: 'critical', + reason: 'LND needs configuration before starting', + }, + }, + connect: { + active: true, + request: { + packageId: 'lnd', + actionId: 'connect', + severity: 'important', + reason: 'View LND connection details', + }, + }, + 'bitcoind/config': { + active: true, + request: { + packageId: 'bitcoind', + actionId: 'config', + severity: 'critical', + reason: 'LND likes BTC a certain way', + input: { + kind: 'partial', + value: { + color: '#ffffff', + testnet: false, + }, + }, }, - 'btc-rpc-proxy': { - title: Mock.MockManifestBitcoinProxy.title, - icon: 'assets/img/service-icons/btc-rpc-proxy.png', + }, + 'bitcoind/rpc': { + active: true, + request: { + packageId: 'bitcoind', + actionId: 'rpc', + severity: 'important', + reason: `LND want's its own RPC credentials`, + input: { + kind: 'partial', + value: { + rpcsettings: { + rpcuser: 'lnd', + }, + }, + }, }, }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, - 'install-progress': undefined, } - export const LocalPkgs: { [key: string]: PackageDataEntry } = { - bitcoind: bitcoind, - 'btc-rpc-proxy': bitcoinProxy, - lnd: lnd, - } + export const LocalPkgs: { [key: string]: PackageDataEntry } = + { + bitcoind: bitcoind, + 'btc-rpc-proxy': bitcoinProxy, + lnd: lnd, + } } diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 9d804caf9..db9b62add 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,20 +1,28 @@ -import { Dump, Revision } from 'patch-db-client' -import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' -import { PackagePropertiesVersioned } from 'src/app/util/properties.util' -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { - DataModel, - HealthCheckResult, - Manifest, -} from 'src/app/services/patch-db/data-model' +import { Dump } from 'patch-db-client' +import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' +import { IST, T } from '@start9labs/start-sdk' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' export module RR { - // DB + // websocket + + export type WebsocketConfig = Omit, 'url'> + + // state + + export type EchoReq = { message: string } // server.echo + export type EchoRes = string + + export type ServerState = 'initializing' | 'error' | 'running' - export type GetRevisionsRes = Revision[] | Dump + // DB - export type GetDumpRes = Dump + export type SubscribePatchReq = {} + export type SubscribePatchRes = { + dump: Dump + guid: string + } export type SetDBValueReq = { pointer: string; value: T } // db.put.ui export type SetDBValueRes = null @@ -24,6 +32,7 @@ export module RR { export type LoginReq = { password: string metadata: SessionMetadata + ephemeral?: boolean } // auth.login - unauthed export type loginRes = null @@ -31,15 +40,27 @@ export module RR { export type LogoutRes = null export type ResetPasswordReq = { - 'old-password': string - 'new-password': string + oldPassword: string + newPassword: string } // auth.reset-password export type ResetPasswordRes = null - // server + // diagnostic - export type EchoReq = { message: string; timeout?: number } // server.echo - export type EchoRes = string + export type DiagnosticErrorRes = { + code: number + message: string + data: { details: string } + } + + // init + + export type InitGetProgressRes = { + progress: T.FullProgress + guid: string + } + + // server export type GetSystemTimeReq = {} // server.time export type GetSystemTimeRes = { @@ -50,16 +71,20 @@ export module RR { export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs export type GetServerLogsRes = LogsRes - export type FollowServerLogsReq = { limit?: number } // server.logs.follow & server.kernel-logs.follow + export type FollowServerLogsReq = { + limit?: number // (optional) default is 50. Ignored if cursor provided + boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined + cursor?: string // the last known log. Websocket will return all logs since this log + } // server.logs.follow & server.kernel-logs.follow export type FollowServerLogsRes = { - 'start-cursor': string + startCursor: string guid: string } export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsRes = Metrics - export type UpdateServerReq = { 'marketplace-url': string } // server.update + export type UpdateServerReq = { registry: string } // server.update export type UpdateServerRes = 'updating' | 'no-updates' export type RestartServerReq = {} // server.restart @@ -68,19 +93,25 @@ export module RR { export type ShutdownServerReq = {} // server.shutdown export type ShutdownServerRes = null - export type SystemRebuildReq = {} // server.rebuild - export type SystemRebuildRes = null + export type DiskRepairReq = {} // server.disk.repair + export type DiskRepairRes = null export type ResetTorReq = { - 'wipe-state': boolean + wipeState: boolean reason: string } // net.tor.reset export type ResetTorRes = null - export type ToggleZramReq = { - enable: boolean - } // server.experimental.zram - export type ToggleZramRes = null + // smtp + + export type SetSMTPReq = T.SmtpValue // server.set-smtp + export type SetSMTPRes = null + + export type ClearSMTPReq = {} // server.clear-smtp + export type ClearSMTPRes = null + + export type TestSMTPReq = SetSMTPReq & { to: string } // server.test-smtp + export type TestSMTPRes = null // sessions @@ -120,7 +151,7 @@ export module RR { connected: string | null country: string | null ethernet: boolean - 'available-wifi': AvailableWifi[] + availableWifi: AvailableWifi[] } export type AddWifiReq = { @@ -169,23 +200,98 @@ export module RR { export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove export type RemoveBackupTargetRes = null - export type GetBackupInfoReq = { 'target-id': string; password: string } // backup.target.info + export type GetBackupInfoReq = { + // backup.target.info + targetId: string + serverId: string + password: string + } export type GetBackupInfoRes = BackupInfo export type CreateBackupReq = { // backup.create - 'target-id': string - 'package-ids': string[] - 'old-password': string | null + targetId: string + packageIds: string[] + oldPassword: string | null password: string } export type CreateBackupRes = null // package - export type GetPackagePropertiesReq = { id: string } // package.properties - export type GetPackagePropertiesRes = - PackagePropertiesVersioned + export type InitAcmeReq = { + provider: 'letsencrypt' | 'letsencrypt-staging' | string + contact: string[] + } + export type InitAcmeRes = null + + export type RemoveAcmeReq = { + provider: string + } + export type RemoveAcmeRes = null + + export type AddTorKeyReq = { + // net.tor.key.add + key: string + } + export type GenerateTorKeyReq = {} // net.tor.key.generate + export type AddTorKeyRes = string // onion address without .onion suffix + + export type ServerBindingSetPublicReq = { + // server.host.binding.set-public + internalPort: number + public: boolean | null // default true + } + export type BindingSetPublicRes = null + + export type ServerAddOnionReq = { + // server.host.address.onion.add + onion: string // address *with* .onion suffix + } + export type AddOnionRes = null + + export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove + export type RemoveOnionRes = null + + export type ServerAddDomainReq = { + // server.host.address.domain.add + domain: string // FQDN + private: boolean + acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null + } + export type AddDomainRes = null + + export type ServerRemoveDomainReq = { + // server.host.address.domain.remove + domain: string // FQDN + } + export type RemoveDomainRes = null + + export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & { + // package.host.binding.set-public + package: T.PackageId // string + host: T.HostId // string + } + + export type PkgAddOnionReq = ServerAddOnionReq & { + // package.host.address.onion.add + package: T.PackageId // string + host: T.HostId // string + } + + export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove + + export type PkgAddDomainReq = ServerAddDomainReq & { + // package.host.address.domain.add + package: T.PackageId // string + host: T.HostId // string + } + + export type PkgRemoveDomainReq = ServerRemoveDomainReq & { + // package.host.address.domain.remove + package: T.PackageId // string + host: T.HostId // string + } export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs export type GetPackageLogsRes = LogsRes @@ -193,42 +299,31 @@ export module RR { export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow export type FollowPackageLogsRes = FollowServerLogsRes - export type GetPackageMetricsReq = { id: string } // package.metrics - export type GetPackageMetricsRes = Metric - - export type InstallPackageReq = { - id: string - 'version-spec'?: string - 'version-priority'?: 'min' | 'max' - 'marketplace-url': string - } // package.install + export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null - export type GetPackageConfigReq = { id: string } // package.config.get - export type GetPackageConfigRes = { spec: ConfigSpec; config: object } - - export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry - export type DrySetPackageConfigRes = Breakages + export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input + export type GetActionInputRes = { + spec: IST.InputSpec + value: object | null + } - export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set - export type SetPackageConfigRes = null + export type ActionReq = { + packageId: string + actionId: string + input: object | null + } // package.action.run + export type ActionRes = (T.ActionResult & { version: '1' }) | null export type RestorePackagesReq = { // package.backup.restore ids: string[] - 'target-id': string - 'old-password': string | null + targetId: string + serverId: string password: string } export type RestorePackagesRes = null - export type ExecutePackageActionReq = { - id: string - 'action-id': string - input?: object - } // package.action - export type ExecutePackageActionRes = ActionResponse - export type StartPackageReq = { id: string } // package.start export type StartPackageRes = null @@ -238,51 +333,34 @@ export module RR { export type StopPackageReq = { id: string } // package.stop export type StopPackageRes = null + export type RebuildPackageReq = { id: string } // package.rebuild + export type RebuildPackageRes = null + export type UninstallPackageReq = { id: string } // package.uninstall export type UninstallPackageRes = null - export type DryConfigureDependencyReq = { - 'dependency-id': string - 'dependent-id': string - } // package.dependency.configure.dry - export type DryConfigureDependencyRes = { - 'old-config': object - 'new-config': object - spec: ConfigSpec - } - export type SideloadPackageReq = { - manifest: Manifest + manifest: T.Manifest icon: string // base64 } - export type SideloadPacakgeRes = string //guid - - // marketplace - - export type GetMarketplaceInfoReq = { 'server-id': string } - export type GetMarketplaceInfoRes = StoreInfo + export type SideloadPackageRes = { + upload: string // guid + progress: string // guid + } - export type GetMarketplaceEosReq = { 'server-id': string } - export type GetMarketplaceEosRes = MarketplaceEOS + // registry - export type GetMarketplacePackagesReq = { - ids?: { id: string; version: string }[] - // iff !ids - category?: string - query?: string - page?: number - 'per-page'?: number - } - export type GetMarketplacePackagesRes = MarketplacePkg[] + /** these are returned in ASCENDING order. the newest available version will be the LAST in the object */ + export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo } - export type GetReleaseNotesReq = { id: string } - export type GetReleaseNotesRes = { [version: string]: string } + export type CheckOSUpdateReq = { serverId: string } + export type CheckOSUpdateRes = OSUpdate } -export interface MarketplaceEOS { +export interface OSUpdate { version: string headline: string - 'release-notes': { [version: string]: string } + releaseNotes: { [version: string]: string } } export interface Breakages { @@ -294,13 +372,6 @@ export interface TaggedDependencyError { error: DependencyError } -export interface ActionResponse { - message: string - value: string | null - copyable: boolean - qr: boolean -} - interface MetricData { value: string unit: string @@ -312,23 +383,23 @@ export interface Metrics { } memory: { total: MetricData - 'percentage-used': MetricData + percentageUsed: MetricData used: MetricData available: MetricData - 'zram-total': MetricData - 'zram-used': MetricData - 'zram-available': MetricData + zramTotal: MetricData + zramUsed: MetricData + zramAvailable: MetricData } cpu: { - 'percentage-used': MetricData + percentageUsed: MetricData idle: MetricData - 'user-space': MetricData - 'kernel-space': MetricData + userSpace: MetricData + kernelSpace: MetricData wait: MetricData } disk: { capacity: MetricData - 'percentage-used': MetricData + percentageUsed: MetricData used: MetricData available: MetricData } @@ -342,8 +413,9 @@ export interface Metric { } export interface Session { - 'last-active': string - 'user-agent': string + loggedIn: string + lastActive: string + userAgent: string metadata: SessionMetadata } @@ -378,7 +450,7 @@ export interface DiskBackupTarget { label: string | null capacity: number used: number | null - 'embassy-os': StartOSDiskInfo | null + startOs: Record } export interface CifsBackupTarget { @@ -387,7 +459,7 @@ export interface CifsBackupTarget { path: string username: string mountable: boolean - 'embassy-os': StartOSDiskInfo | null + startOs: Record } export type RecoverySource = DiskRecoverySource | CifsRecoverySource @@ -408,7 +480,7 @@ export interface CifsRecoverySource { export interface BackupInfo { version: string timestamp: string - 'package-backups': { + packageBackups: { [id: string]: PackageBackupInfo } } @@ -416,7 +488,7 @@ export interface BackupInfo { export interface PackageBackupInfo { title: string version: string - 'os-version': string + osVersion: string timestamp: string } @@ -425,7 +497,7 @@ export interface ServerSpecs { } export interface SSHKey { - 'created-at': string + createdAt: string alg: string hostname: string fingerprint: string @@ -435,8 +507,8 @@ export type ServerNotifications = ServerNotification[] export interface ServerNotification { id: number - 'package-id': string | null - 'created-at': string + packageId: string | null + createdAt: string code: T level: NotificationLevel title: string @@ -455,6 +527,8 @@ export type NotificationData = T extends 0 ? null : T extends 1 ? BackupReport + : T extends 2 + ? string : any export interface BackupReport { @@ -498,44 +572,33 @@ export type DependencyError = | DependencyErrorNotInstalled | DependencyErrorNotRunning | DependencyErrorIncorrectVersion - | DependencyErrorConfigUnsatisfied + | DependencyErrorActionRequired | DependencyErrorHealthChecksFailed | DependencyErrorTransitive -export enum DependencyErrorType { - NotInstalled = 'not-installed', - NotRunning = 'not-running', - IncorrectVersion = 'incorrect-version', - ConfigUnsatisfied = 'config-unsatisfied', - HealthChecksFailed = 'health-checks-failed', - InterfaceHealthChecksFailed = 'interface-health-checks-failed', - Transitive = 'transitive', -} - export interface DependencyErrorNotInstalled { - type: DependencyErrorType.NotInstalled + type: 'notInstalled' } export interface DependencyErrorNotRunning { - type: DependencyErrorType.NotRunning + type: 'notRunning' } export interface DependencyErrorIncorrectVersion { - type: DependencyErrorType.IncorrectVersion + type: 'incorrectVersion' expected: string // version range received: string // version } -export interface DependencyErrorConfigUnsatisfied { - type: DependencyErrorType.ConfigUnsatisfied - error: string +export interface DependencyErrorActionRequired { + type: 'actionRequired' } export interface DependencyErrorHealthChecksFailed { - type: DependencyErrorType.HealthChecksFailed - check: HealthCheckResult + type: 'healthChecksFailed' + check: T.NamedHealthCheckResult } export interface DependencyErrorTransitive { - type: DependencyErrorType.Transitive + type: 'transitive' } diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 17ce2d9d6..d70e9b669 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,21 +1,50 @@ import { Observable } from 'rxjs' -import { Update } from 'patch-db-client' import { RR } from './api.types' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { Log } from '@start9labs/shared' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { RPCOptions } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' +import { WebSocketSubject } from 'rxjs/webSocket' export abstract class ApiService { // http + // for sideloading packages + abstract uploadPackage(guid: string, body: Blob): Promise + // for getting static files: ex icons, instructions, licenses - abstract getStatic(url: string): Promise + abstract getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise - // for sideloading packages - abstract uploadPackage(guid: string, body: Blob): Promise + abstract getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + + // websocket + + abstract openWebsocket$( + guid: string, + config?: RR.WebsocketConfig, + ): WebSocketSubject + + // state + + abstract echo(params: RR.EchoReq, url: string): Promise + + abstract getState(): Promise // db + abstract subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise + abstract setDbValue( pathArr: Array, value: T, @@ -35,15 +64,25 @@ export abstract class ApiService { params: RR.ResetPasswordReq, ): Promise - // server + // diagnostic + + abstract diagnosticGetError(): Promise + abstract diagnosticRestart(): Promise + abstract diagnosticForgetDrive(): Promise + abstract diagnosticRepairDisk(): Promise + abstract diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise + + // init - abstract echo(params: RR.EchoReq, urlOverride?: string): Promise + abstract initGetProgress(): Promise - abstract openPatchWebsocket$(): Observable> + abstract initFollowLogs( + params: RR.FollowServerLogsReq, + ): Promise - abstract openLogsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable + // server abstract getSystemTime( params: RR.GetSystemTimeReq, @@ -75,10 +114,6 @@ export abstract class ApiService { params: RR.GetServerMetricsReq, ): Promise - abstract getPkgMetrics( - params: RR.GetPackageMetricsReq, - ): Promise - abstract updateServer(url?: string): Promise abstract restartServer( @@ -89,25 +124,36 @@ export abstract class ApiService { params: RR.ShutdownServerReq, ): Promise - abstract systemRebuild( - params: RR.SystemRebuildReq, - ): Promise - - abstract repairDisk(params: RR.SystemRebuildReq): Promise + abstract repairDisk(params: RR.DiskRepairReq): Promise abstract resetTor(params: RR.ResetTorReq): Promise - abstract toggleZram(params: RR.ToggleZramReq): Promise + // smtp + + abstract setSmtp(params: RR.SetSMTPReq): Promise + + abstract clearSmtp(params: RR.ClearSMTPReq): Promise + + abstract testSmtp(params: RR.TestSMTPReq): Promise // marketplace URLs - abstract marketplaceProxy( - path: string, - params: Record, - url: string, + abstract registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise - abstract getEos(): Promise + abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise + + abstract getRegistryInfo(registryUrl: string): Promise + + abstract getRegistryPackage( + url: string, + id: string, + versionRange: string | null, + ): Promise + + abstract getRegistryPackages(registryUrl: string): Promise // notification @@ -174,10 +220,6 @@ export abstract class ApiService { // package - abstract getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> - abstract getPackageLogs( params: RR.GetPackageLogsReq, ): Promise @@ -190,26 +232,16 @@ export abstract class ApiService { params: RR.InstallPackageReq, ): Promise - abstract getPackageConfig( - params: RR.GetPackageConfigReq, - ): Promise + abstract getActionInput( + params: RR.GetActionInputReq, + ): Promise - abstract drySetPackageConfig( - params: RR.DrySetPackageConfigReq, - ): Promise - - abstract setPackageConfig( - params: RR.SetPackageConfigReq, - ): Promise + abstract runAction(params: RR.ActionReq): Promise abstract restorePackages( params: RR.RestorePackagesReq, ): Promise - abstract executePackageAction( - params: RR.ExecutePackageActionReq, - ): Promise - abstract startPackage(params: RR.StartPackageReq): Promise abstract restartPackage( @@ -218,15 +250,57 @@ export abstract class ApiService { abstract stopPackage(params: RR.StopPackageReq): Promise + abstract rebuildPackage( + params: RR.RebuildPackageReq, + ): Promise + abstract uninstallPackage( params: RR.UninstallPackageReq, ): Promise - abstract dryConfigureDependency( - params: RR.DryConfigureDependencyReq, - ): Promise + abstract sideloadPackage(): Promise + + abstract initAcme(params: RR.InitAcmeReq): Promise + + abstract removeAcme(params: RR.RemoveAcmeReq): Promise + + abstract addTorKey(params: RR.AddTorKeyReq): Promise + + abstract generateTorKey( + params: RR.GenerateTorKeyReq, + ): Promise + + abstract serverBindingSetPubic( + params: RR.ServerBindingSetPublicReq, + ): Promise + + abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise + + abstract serverRemoveOnion( + params: RR.ServerRemoveOnionReq, + ): Promise + + abstract serverAddDomain( + params: RR.ServerAddDomainReq, + ): Promise + + abstract serverRemoveDomain( + params: RR.ServerRemoveDomainReq, + ): Promise + + abstract pkgBindingSetPubic( + params: RR.PkgBindingSetPublicReq, + ): Promise + + abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise + + abstract pkgRemoveOnion( + params: RR.PkgRemoveOnionReq, + ): Promise + + abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise - abstract sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise + abstract pkgRemoveDomain( + params: RR.PkgRemoveDomainReq, + ): Promise } diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 18d47e2ce..34082c033 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -3,22 +3,29 @@ import { HttpOptions, HttpService, isRpcError, - Log, Method, RpcError, RPCOptions, } from '@start9labs/shared' +import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source' import { ApiService } from './embassy-api.service' import { RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { ConfigService } from '../config.service' -import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' +import { webSocket, WebSocketSubject } from 'rxjs/webSocket' import { Observable, filter, firstValueFrom } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' -import { PatchDB, pathFromArray, Update } from 'patch-db-client' -import { getServerInfo } from 'src/app/util/get-server-info' +import { Dump, pathFromArray } from 'patch-db-client' +import { T } from '@start9labs/start-sdk' +import { + GetPackageReq, + GetPackageRes, + GetPackagesReq, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' +import { blake3 } from '@noble/hashes/blake3' @Injectable() export class LiveApiService extends ApiService { @@ -27,33 +34,86 @@ export class LiveApiService extends ApiService { private readonly http: HttpService, private readonly config: ConfigService, private readonly auth: AuthService, - private readonly patch: PatchDB, + @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() ; (window as any).rpcClient = this } - // for getting static files: ex icons, instructions, licenses - async getStatic(url: string): Promise { + // for sideloading packages + + async uploadPackage(guid: string, body: Blob): Promise { + await this.httpRequest({ + method: Method.POST, + body, + url: `/rest/rpc/${guid}`, + }) + } + + // for getting static files: ex. instructions, licenses + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + const encodedUrl = encodeURIComponent(pkg.s9pk.url) + return this.httpRequest({ method: Method.GET, - url, + url: `/s9pk/proxy/${encodedUrl}/${path}`, + params: { + rootSighash: pkg.s9pk.commitment.rootSighash, + rootMaxsize: pkg.s9pk.commitment.rootMaxsize, + }, responseType: 'text', }) } - // for sideloading packages - async uploadPackage(guid: string, body: Blob): Promise { + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { return this.httpRequest({ - method: Method.POST, - body, - url: `/rest/rpc/${guid}`, + method: Method.GET, + url: `/s9pk/installed/${id}.s9pk/${path}`, responseType: 'text', }) } + // websocket + + openWebsocket$( + guid: string, + config: RR.WebsocketConfig = {}, + ): WebSocketSubject { + const { location } = this.document.defaultView! + const protocol = location.protocol === 'http:' ? 'ws' : 'wss' + const host = location.host + + return webSocket({ + url: `${protocol}://${host}/ws/rpc/${guid}`, + ...config, + }) + } + + // state + + async echo(params: RR.EchoReq, url: string): Promise { + return this.rpcRequest({ method: 'echo', params }, url) + } + + async getState(): Promise { + return this.rpcRequest({ method: 'state', params: {} }) + } + // db + async subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise { + return this.rpcRequest({ method: 'db.subscribe', params }) + } + async setDbValue( pathArr: Array, value: T, @@ -87,29 +147,59 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'auth.reset-password', params }) } - // server + // diagnostic - async echo(params: RR.EchoReq, urlOverride?: string): Promise { - return this.rpcRequest({ method: 'echo', params }, urlOverride) + async diagnosticGetError(): Promise { + return this.rpcRequest({ + method: 'diagnostic.error', + params: {}, + }) } - openPatchWebsocket$(): Observable> { - const config: WebSocketSubjectConfig> = { - url: `/db`, - closeObserver: { - next: val => { - if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified() - }, - }, - } + async diagnosticRestart(): Promise { + return this.rpcRequest({ + method: 'diagnostic.restart', + params: {}, + }) + } + + async diagnosticForgetDrive(): Promise { + return this.rpcRequest({ + method: 'diagnostic.disk.forget', + params: {}, + }) + } - return this.openWebsocket(config) + async diagnosticRepairDisk(): Promise { + return this.rpcRequest({ + method: 'diagnostic.disk.repair', + params: {}, + }) } - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return this.openWebsocket(config) + async diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise { + return this.rpcRequest({ + method: 'diagnostic.logs', + params, + }) } + // init + + async initGetProgress(): Promise { + return this.rpcRequest({ method: 'init.subscribe', params: {} }) + } + + async initFollowLogs( + params: RR.FollowServerLogsReq, + ): Promise { + return this.rpcRequest({ method: 'init.logs.follow', params }) + } + + // server + async getSystemTime( params: RR.GetSystemTimeReq, ): Promise { @@ -158,7 +248,7 @@ export class LiveApiService extends ApiService { async updateServer(url?: string): Promise { const params = { - 'marketplace-url': url || this.config.marketplace.start9, + registry: url || this.config.marketplace.start9, } return this.rpcRequest({ method: 'server.update', params }) } @@ -175,12 +265,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.shutdown', params }) } - async systemRebuild( - params: RR.RestartServerReq, - ): Promise { - return this.rpcRequest({ method: 'server.rebuild', params }) - } - async repairDisk(params: RR.RestartServerReq): Promise { return this.rpcRequest({ method: 'disk.repair', params }) } @@ -189,33 +273,63 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'net.tor.reset', params }) } - async toggleZram(params: RR.ToggleZramReq): Promise { - return this.rpcRequest({ method: 'server.experimental.zram', params }) - } - // marketplace URLs - async marketplaceProxy( - path: string, - qp: Record, - baseUrl: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { - const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` return this.rpcRequest({ - method: 'marketplace.get', - params: { url: fullUrl }, + ...options, + method: `registry.${options.method}`, + params: { registry: registryUrl, ...options.params }, }) } - async getEos(): Promise { - const { id } = await getServerInfo(this.patch) - const qp: RR.GetMarketplaceEosReq = { 'server-id': id } + async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { + const { serverId } = qp - return this.marketplaceProxy( - '/eos/v0/latest', - qp, - this.config.marketplace.start9, - ) + return this.registryRequest(this.config.marketplace.start9, { + method: 'os.version.get', + params: { serverId }, + }) + } + + async getRegistryInfo(registryUrl: string): Promise { + return this.registryRequest(registryUrl, { + method: 'info', + params: {}, + }) + } + + async getRegistryPackage( + registryUrl: string, + id: string, + versionRange: string | null, + ): Promise { + const params: GetPackageReq = { + id, + version: versionRange, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) + } + + async getRegistryPackages(registryUrl: string): Promise { + const params: GetPackagesReq = { + id: null, + version: null, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) } // notification @@ -268,6 +382,20 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'wifi.delete', params }) } + // smtp + + async setSmtp(params: RR.SetSMTPReq): Promise { + return this.rpcRequest({ method: 'server.set-smtp', params }) + } + + async clearSmtp(params: RR.ClearSMTPReq): Promise { + return this.rpcRequest({ method: 'server.clear-smtp', params }) + } + + async testSmtp(params: RR.TestSMTPReq): Promise { + return this.rpcRequest({ method: 'server.test-smtp', params }) + } + // ssh async getSshKeys(params: RR.GetSSHKeysReq): Promise { @@ -321,14 +449,6 @@ export class LiveApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { - return this.rpcRequest({ method: 'package.properties', params }).then( - parsePropertiesPermissive, - ) - } - async getPackageLogs( params: RR.GetPackageLogsReq, ): Promise { @@ -336,39 +456,25 @@ export class LiveApiService extends ApiService { } async followPackageLogs( - params: RR.FollowServerLogsReq, - ): Promise { + params: RR.FollowPackageLogsReq, + ): Promise { return this.rpcRequest({ method: 'package.logs.follow', params }) } - async getPkgMetrics( - params: RR.GetPackageMetricsReq, - ): Promise { - return this.rpcRequest({ method: 'package.metrics', params }) - } - async installPackage( params: RR.InstallPackageReq, ): Promise { return this.rpcRequest({ method: 'package.install', params }) } - async getPackageConfig( - params: RR.GetPackageConfigReq, - ): Promise { - return this.rpcRequest({ method: 'package.config.get', params }) - } - - async drySetPackageConfig( - params: RR.DrySetPackageConfigReq, - ): Promise { - return this.rpcRequest({ method: 'package.config.set.dry', params }) + async getActionInput( + params: RR.GetActionInputReq, + ): Promise { + return this.rpcRequest({ method: 'package.action.get-input', params }) } - async setPackageConfig( - params: RR.SetPackageConfigReq, - ): Promise { - return this.rpcRequest({ method: 'package.config.set', params }) + async runAction(params: RR.ActionReq): Promise { + return this.rpcRequest({ method: 'package.action.run', params }) } async restorePackages( @@ -377,12 +483,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'package.backup.restore', params }) } - async executePackageAction( - params: RR.ExecutePackageActionReq, - ): Promise { - return this.rpcRequest({ method: 'package.action', params }) - } - async startPackage(params: RR.StartPackageReq): Promise { return this.rpcRequest({ method: 'package.start', params }) } @@ -397,38 +497,135 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'package.stop', params }) } + async rebuildPackage( + params: RR.RebuildPackageReq, + ): Promise { + return this.rpcRequest({ method: 'package.rebuild', params }) + } + async uninstallPackage( params: RR.UninstallPackageReq, ): Promise { return this.rpcRequest({ method: 'package.uninstall', params }) } - async dryConfigureDependency( - params: RR.DryConfigureDependencyReq, - ): Promise { + async sideloadPackage(): Promise { + return this.rpcRequest({ + method: 'package.sideload', + params: {}, + }) + } + + async removeAcme(params: RR.RemoveAcmeReq): Promise { return this.rpcRequest({ - method: 'package.dependency.configure.dry', + method: 'net.acme.delete', params, }) } - async sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise { + async initAcme(params: RR.InitAcmeReq): Promise { return this.rpcRequest({ - method: 'package.sideload', + method: 'net.acme.init', params, }) } - private openWebsocket(config: WebSocketSubjectConfig): Observable { - const { location } = this.document.defaultView! - const protocol = location.protocol === 'http:' ? 'ws' : 'wss' - const host = location.host + async addTorKey(params: RR.AddTorKeyReq): Promise { + return this.rpcRequest({ + method: 'net.tor.key.add', + params, + }) + } + + async generateTorKey(params: RR.GenerateTorKeyReq): Promise { + return this.rpcRequest({ + method: 'net.tor.key.generate', + params, + }) + } + + async serverBindingSetPubic( + params: RR.ServerBindingSetPublicReq, + ): Promise { + return this.rpcRequest({ + method: 'server.host.binding.set-public', + params, + }) + } + + async serverAddOnion(params: RR.ServerAddOnionReq): Promise { + return this.rpcRequest({ + method: 'server.host.address.onion.add', + params, + }) + } - config.url = `${protocol}://${host}/ws${config.url}` + async serverRemoveOnion( + params: RR.ServerRemoveOnionReq, + ): Promise { + return this.rpcRequest({ + method: 'server.host.address.onion.remove', + params, + }) + } - return webSocket(config) + async serverAddDomain( + params: RR.ServerAddDomainReq, + ): Promise { + return this.rpcRequest({ + method: 'server.host.address.domain.add', + params, + }) + } + + async serverRemoveDomain( + params: RR.ServerRemoveDomainReq, + ): Promise { + return this.rpcRequest({ + method: 'server.host.address.domain.remove', + params, + }) + } + + async pkgBindingSetPubic( + params: RR.PkgBindingSetPublicReq, + ): Promise { + return this.rpcRequest({ + method: 'package.host.binding.set-public', + params, + }) + } + + async pkgAddOnion(params: RR.PkgAddOnionReq): Promise { + return this.rpcRequest({ + method: 'package.host.address.onion.add', + params, + }) + } + + async pkgRemoveOnion( + params: RR.PkgRemoveOnionReq, + ): Promise { + return this.rpcRequest({ + method: 'package.host.address.onion.remove', + params, + }) + } + + async pkgAddDomain(params: RR.PkgAddDomainReq): Promise { + return this.rpcRequest({ + method: 'package.host.address.domain.add', + params, + }) + } + + async pkgRemoveDomain( + params: RR.PkgRemoveDomainReq, + ): Promise { + return this.rpcRequest({ + method: 'package.host.address.domain.remove', + params, + }) } private async rpcRequest( @@ -449,9 +646,7 @@ export class LiveApiService extends ApiService { const patchSequence = res.headers.get('x-patch-sequence') if (patchSequence) await firstValueFrom( - this.patch.cache$.pipe( - filter(({ sequence }) => sequence >= Number(patchSequence)), - ), + this.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))), ) return body.result @@ -459,6 +654,29 @@ export class LiveApiService extends ApiService { private async httpRequest(opts: HttpOptions): Promise { const res = await this.http.httpRequest(opts) + if (res.headers.get('Repr-Digest')) { + // verify + const digest = res.headers.get('Repr-Digest')! + let data: Uint8Array + if (opts.responseType === 'arrayBuffer') { + data = Buffer.from(res.body as ArrayBuffer) + } else if (opts.responseType === 'text') { + data = Buffer.from(res.body as string) + } else if ((opts.responseType as string) === 'blob') { + data = Buffer.from(await (res.body as Blob).arrayBuffer()) + } else { + console.warn( + `could not verify Repr-Digest for responseType ${ + opts.responseType || 'json' + }`, + ) + return res.body + } + const computedDigest = Buffer.from(blake3(data)).toString('base64') + if (`blake3=:${computedDigest}:` === digest) return res.body + console.debug(computedDigest, digest) + throw new Error('File digest mismatch.') + } return res.body } } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index c98a4bd87..31a021416 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,98 +1,166 @@ import { Injectable } from '@angular/core' -import { Log, pauseFor } from '@start9labs/shared' +import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { + AddOperation, Operation, PatchOp, pathFromArray, RemoveOperation, - Update, + ReplaceOperation, + Revision, } from 'patch-db-client' import { - DataModel, - InstallProgress, + InstallingState, PackageDataEntry, - PackageMainStatus, - PackageState, - ServerStatus, + StateInfo, + UpdatingState, } from 'src/app/services/patch-db/data-model' import { CifsBackupTarget, RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' -import { - EMPTY, - iif, - interval, - map, - Observable, - shareReplay, - Subject, - switchMap, - tap, - timer, -} from 'rxjs' -import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' +import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs' import { mockPatchData } from './mock-patch' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { AuthService } from '../auth.service' -import { ConnectionService } from '../connection.service' -import { StoreInfo } from '@start9labs/marketplace' - -const PROGRESS: InstallProgress = { - size: 120, - downloaded: 0, - 'download-complete': false, - validated: 0, - 'validation-complete': false, - unpacked: 0, - 'unpack-complete': false, +import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' +import { WebSocketSubject } from 'rxjs/webSocket' +import { toAcmeUrl } from 'src/app/util/acme' + +const PROGRESS: T.FullProgress = { + overall: { + done: 0, + total: 120, + }, + phases: [ + { + name: 'Downloading', + progress: { + done: 0, + total: 40, + }, + }, + { + name: 'Validating', + progress: null, + }, + { + name: 'Installing', + progress: { + done: 0, + total: 40, + }, + }, + ], } @Injectable() export class MockApiService extends ApiService { - readonly mockWsSource$ = new Subject>() + readonly mockWsSource$ = new Subject() private readonly revertTime = 1800 sequence = 0 - constructor( - private readonly bootstrapper: LocalStorageBootstrap, - private readonly connectionService: ConnectionService, - private readonly auth: AuthService, - ) { + constructor(private readonly auth: AuthService) { super() this.auth.isVerified$ .pipe( tap(() => { this.sequence = 0 }), - switchMap(verified => - iif( - () => verified, - timer(2000).pipe( - tap(() => { - this.connectionService.websocketConnected$.next(true) - }), - ), - EMPTY, - ), - ), ) .subscribe() } - async getStatic(url: string): Promise { + async uploadPackage(guid: string, body: Blob): Promise { + await pauseFor(2000) + } + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) return markdown } - async uploadPackage(guid: string, body: Blob): Promise { + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) - return 'success' + return markdown + } + + // websocket + + openWebsocket$( + guid: string, + config: RR.WebsocketConfig = {}, + ): WebSocketSubject { + if (guid === 'db-guid') { + return this.mockWsSource$.pipe( + shareReplay({ bufferSize: 1, refCount: true }), + ) as WebSocketSubject + } else if (guid === 'logs-guid') { + return interval(50).pipe( + map((_, index) => { + // mock fire open observer + if (index === 0) config.openObserver?.next(new Event('')) + if (index === 100) throw new Error('HAAHHA') + return Mock.ServerLogs[0] + }), + ) as WebSocketSubject + } else if (guid === 'init-progress-guid') { + return from(this.initProgress()).pipe( + startWith(PROGRESS), + ) as WebSocketSubject + } else if (guid === 'sideload-progress-guid') { + config.openObserver?.next(new Event('')) + return from(this.initProgress()).pipe( + startWith(PROGRESS), + ) as WebSocketSubject + } else { + throw new Error('invalid guid type') + } + } + + // state + + async echo(params: RR.EchoReq, url: string): Promise { + if (url) { + const num = Math.floor(Math.random() * 10) + 1 + if (num > 8) return params.message + throw new Error() + } + await pauseFor(2000) + return params.message + } + + private stateIndex = 0 + async getState(): Promise { + await pauseFor(1000) + + this.stateIndex++ + + return this.stateIndex === 1 ? 'running' : 'running' } // db + async subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise { + await pauseFor(2000) + return { + dump: { id: 1, value: mockPatchData }, + guid: 'db-guid', + } + } + async setDbValue( pathArr: Array, value: T, @@ -116,11 +184,6 @@ export class MockApiService extends ApiService { async login(params: RR.LoginReq): Promise { await pauseFor(2000) - - setTimeout(() => { - this.mockWsSource$.next({ id: 1, value: mockPatchData }) - }, 2000) - return null } @@ -146,35 +209,64 @@ export class MockApiService extends ApiService { return null } - // server + // diagnostic - async echo(params: RR.EchoReq, url?: string): Promise { - if (url) { - const num = Math.floor(Math.random() * 10) + 1 - if (num > 8) return params.message - throw new Error() + async getError(): Promise { + await pauseFor(1000) + return { + code: 15, + message: 'Unknown server', + data: { details: 'Some details about the error here' }, } - await pauseFor(2000) - return params.message } - openPatchWebsocket$(): Observable> { - return this.mockWsSource$.pipe( - shareReplay({ bufferSize: 1, refCount: true }), - ) + async diagnosticGetError(): Promise { + await pauseFor(1000) + return { + code: 15, + message: 'Unknown server', + data: { details: 'Some details about the error here' }, + } + } + + async diagnosticRestart(): Promise { + await pauseFor(1000) + } + + async diagnosticForgetDrive(): Promise { + await pauseFor(1000) + } + + async diagnosticRepairDisk(): Promise { + await pauseFor(1000) + } + + async diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise { + return this.getServerLogs(params) + } + + // init + + async initGetProgress(): Promise { + await pauseFor(250) + return { + progress: PROGRESS, + guid: 'init-progress-guid', + } } - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return interval(50).pipe( - map((_, index) => { - // mock fire open observer - if (index === 0) config.openObserver?.next(new Event('')) - if (index === 100) throw new Error('HAAHHA') - return Mock.ServerLogs[0] - }), - ) + async initFollowLogs(): Promise { + await pauseFor(2000) + return { + startCursor: 'start-cursor', + guid: 'logs-guid', + } } + // server + async getSystemTime( params: RR.GetSystemTimeReq, ): Promise { @@ -193,8 +285,8 @@ export class MockApiService extends ApiService { return { entries, - 'start-cursor': 'startCursor', - 'end-cursor': 'endCursor', + startCursor: 'start-cursor', + endCursor: 'end-cursor', } } @@ -206,8 +298,8 @@ export class MockApiService extends ApiService { return { entries, - 'start-cursor': 'startCursor', - 'end-cursor': 'endCursor', + startCursor: 'start-cursor', + endCursor: 'end-cursor', } } @@ -217,8 +309,8 @@ export class MockApiService extends ApiService { return { entries, - 'start-cursor': 'startCursor', - 'end-cursor': 'endCursor', + startCursor: 'startCursor', + endCursor: 'end-cursor', } } @@ -227,8 +319,8 @@ export class MockApiService extends ApiService { ): Promise { await pauseFor(2000) return { - 'start-cursor': 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + startCursor: 'start-cursor', + guid: 'logs-guid', } } @@ -237,8 +329,8 @@ export class MockApiService extends ApiService { ): Promise { await pauseFor(2000) return { - 'start-cursor': 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + startCursor: 'start-cursor', + guid: 'logs-guid', } } @@ -247,12 +339,12 @@ export class MockApiService extends ApiService { ): Promise { await pauseFor(2000) return { - 'start-cursor': 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + startCursor: 'start-cursor', + guid: 'logs-guid', } } - randomLogs(limit = 1): Log[] { + private randomLogs(limit = 1): Log[] { const arrLength = Math.ceil(limit / Mock.ServerLogs.length) const logs = new Array(arrLength) .fill(Mock.ServerLogs) @@ -268,13 +360,6 @@ export class MockApiService extends ApiService { return Mock.getServerMetrics() } - async getPkgMetrics( - params: RR.GetServerMetricsReq, - ): Promise { - await pauseFor(2000) - return Mock.getAppMetrics() - } - async updateServer(url?: string): Promise { await pauseFor(2000) const initialProgress = { @@ -289,7 +374,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info/update-progress', + path: '/serverInfo/statusInfo/updateProgress', value: initialProgress, }, ] @@ -306,7 +391,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info/restarting', + path: '/serverInfo/statusInfo/restarting', value: true, }, ] @@ -316,7 +401,7 @@ export class MockApiService extends ApiService { const patch2 = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info/restarting', + path: '/serverInfo/statusInfo/restarting', value: false, }, ] @@ -334,7 +419,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info/shutting-down', + path: '/serverInfo/statusInfo/shuttingDown', value: true, }, ] @@ -344,7 +429,7 @@ export class MockApiService extends ApiService { const patch2 = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info/shutting-down', + path: '/serverInfo/statusInfo/shuttingDown', value: false, }, ] @@ -354,12 +439,6 @@ export class MockApiService extends ApiService { return null } - async systemRebuild( - params: RR.SystemRebuildReq, - ): Promise { - return this.restartServer(params) - } - async repairDisk(params: RR.RestartServerReq): Promise { await pauseFor(2000) return null @@ -370,55 +449,43 @@ export class MockApiService extends ApiService { return null } - async toggleZram(params: RR.ToggleZramReq): Promise { + // marketplace URLs + + async registryRequest( + registryUrl: string, + options: RPCOptions, + ): Promise { await pauseFor(2000) - const patch = [ - { - op: PatchOp.REPLACE, - path: '/server-info/zram', - value: params.enable, - }, - ] - this.mockRevision(patch) - return null + return Error('do not call directly') } - // marketplace URLs + async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { + await pauseFor(2000) + return Mock.MarketplaceEos + } - async marketplaceProxy( - path: string, - params: Record, - url: string, - ): Promise { + async getRegistryInfo(registryUrl: string): Promise { await pauseFor(2000) + return Mock.RegistryInfo + } - if (path === '/package/v0/info') { - const info: StoreInfo = { - name: 'Start9 Registry', - categories: [ - 'bitcoin', - 'lightning', - 'data', - 'featured', - 'messaging', - 'social', - 'alt coin', - ], - } - return info - } else if (path === '/package/v0/index') { - return Mock.MarketplacePkgsList - } else if (path.startsWith('/package/v0/release-notes')) { - return Mock.ReleaseNotes - } else if (path.includes('instructions') || path.includes('license')) { - return markdown + async getRegistryPackage( + url: string, + id: string, + versionRange: string, + ): Promise { + await pauseFor(2000) + if (!versionRange) { + return Mock.RegistryPackages[id] + } else { + return Mock.OtherPackageVersions[id][versionRange] } } - async getEos(): Promise { + async getRegistryPackages(registryUrl: string): Promise { await pauseFor(2000) - return Mock.MarketplaceEos + return Mock.RegistryPackages } // notification @@ -430,7 +497,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/unread-notification-count', + path: '/serverInfo/unreadNotificationCount', value: 0, }, ] @@ -482,6 +549,41 @@ export class MockApiService extends ApiService { return null } + // smtp + + async setSmtp(params: RR.SetSMTPReq): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/serverInfo/smtp', + value: params, + }, + ] + this.mockRevision(patch) + + return null + } + + async clearSmtp(params: RR.ClearSMTPReq): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/serverInfo/smtp', + value: null, + }, + ] + this.mockRevision(patch) + + return null + } + + async testSmtp(params: RR.TestSMTPReq): Promise { + await pauseFor(2000) + return null + } + // ssh async getSshKeys(params: RR.GetSSHKeysReq): Promise { @@ -520,7 +622,7 @@ export class MockApiService extends ApiService { path: path.replace(/\\/g, '/'), username, mountable: true, - 'embassy-os': null, + startOs: {}, }, } } @@ -556,18 +658,18 @@ export class MockApiService extends ApiService { async createBackup(params: RR.CreateBackupReq): Promise { await pauseFor(2000) - const path = '/server-info/status-info/backup-progress' - const ids = params['package-ids'] + const serverPath = '/serverInfo/statusInfo/backupProgress' + const ids = params.packageIds setTimeout(async () => { for (let i = 0; i < ids.length; i++) { const id = ids[i] - const appPath = `/package-data/${id}/installed/status/main/status` - const appPatch = [ + const appPath = `/packageData/${id}/status/main/` + const appPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: appPath, - value: PackageMainStatus.BackingUp, + value: 'backingUp', }, ] this.mockRevision(appPatch) @@ -577,43 +679,46 @@ export class MockApiService extends ApiService { this.mockRevision([ { ...appPatch[0], - value: PackageMainStatus.Stopped, + value: 'stopped', }, ]) - this.mockRevision([ + + const serverPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: `${path}/${id}/complete`, + path: `${serverPath}/${id}/complete`, value: true, }, - ]) + ] + this.mockRevision(serverPatch) } await pauseFor(1000) - // set server back to running - const lastPatch = [ + // remove backupProgress + const lastPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path, + path: serverPath, value: null, }, ] this.mockRevision(lastPatch) }, 500) - const originalPatch = [ - { - op: PatchOp.REPLACE, - path, - value: ids.reduce((acc, val) => { - return { - ...acc, - [val]: { complete: false }, - } - }, {}), - }, - ] + const originalPatch: ReplaceOperation[] = + [ + { + op: PatchOp.REPLACE, + path: serverPath, + value: ids.reduce((acc, val) => { + return { + ...acc, + [val]: { complete: false }, + } + }, {}), + }, + ] this.mockRevision(originalPatch) @@ -622,32 +727,25 @@ export class MockApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { - await pauseFor(2000) - return parsePropertiesPermissive(Mock.PackageProperties) - } - async getPackageLogs( params: RR.GetPackageLogsReq, ): Promise { await pauseFor(2000) let entries if (Math.random() < 0.2) { - entries = Mock.PackageLogs + entries = Mock.ServerLogs } else { const arrLength = params.limit - ? Math.ceil(params.limit / Mock.PackageLogs.length) + ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10 entries = new Array(arrLength) - .fill(Mock.PackageLogs) + .fill(Mock.ServerLogs) .reduce((acc, val) => acc.concat(val), []) } return { entries, - 'start-cursor': 'startCursor', - 'end-cursor': 'endCursor', + startCursor: 'startCursor', + endCursor: 'end-cursor', } } @@ -656,8 +754,8 @@ export class MockApiService extends ApiService { ): Promise { await pauseFor(2000) return { - 'start-cursor': 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + startCursor: 'start-cursor', + guid: 'logs-guid', } } @@ -667,18 +765,31 @@ export class MockApiService extends ApiService { await pauseFor(2000) setTimeout(async () => { - this.updateProgress(params.id) + this.installProgress(params.id) }, 1000) - const patch: Operation[] = [ + const patch: AddOperation< + PackageDataEntry + >[] = [ { op: PatchOp.ADD, - path: `/package-data/${params.id}`, + path: `/packageData/${params.id}`, value: { ...Mock.LocalPkgs[params.id], - // state: PackageState.Installing, - state: PackageState.Updating, - 'install-progress': { ...PROGRESS }, + stateInfo: { + // if installing + state: 'installing', + + // if updating + // state: 'updating', + // manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, + + // both + installingInfo: { + newManifest: Mock.LocalPkgs[params.id].stateInfo.manifest, + progress: PROGRESS, + }, + }, }, }, ] @@ -687,56 +798,59 @@ export class MockApiService extends ApiService { return null } - async getPackageConfig( - params: RR.GetPackageConfigReq, - ): Promise { + async getActionInput( + params: RR.GetActionInputReq, + ): Promise { await pauseFor(2000) return { - config: Mock.MockConfig, - spec: Mock.ConfigSpec, + value: Mock.MockConfig, + spec: await Mock.getActionInputSpec(), } } - async drySetPackageConfig( - params: RR.DrySetPackageConfigReq, - ): Promise { + async runAction(params: RR.ActionReq): Promise { await pauseFor(2000) - return {} - } - - async setPackageConfig( - params: RR.SetPackageConfigReq, - ): Promise { - await pauseFor(2000) - const patch = [ - { - op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/configured`, - value: true, - }, - ] - this.mockRevision(patch) - return null + if (params.actionId === 'properties') { + // return Mock.ActionResGroup + return Mock.ActionResMessage + // return Mock.ActionResSingle + } else if (params.actionId === 'config') { + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/packageData/${params.packageId}/requestedActions/${params.packageId}-config`, + }, + ] + this.mockRevision(patch) + return null + } else { + return Mock.ActionResMessage + // return Mock.ActionResSingle + } } async restorePackages( params: RR.RestorePackagesReq, ): Promise { await pauseFor(2000) - const patch: Operation[] = params.ids.map(id => { + const patch: AddOperation[] = params.ids.map(id => { setTimeout(async () => { - this.updateProgress(id) + this.installProgress(id) }, 2000) return { op: PatchOp.ADD, - path: `/package-data/${id}`, + path: `/packageData/${id}`, value: { ...Mock.LocalPkgs[id], - state: PackageState.Restoring, - 'install-progress': { ...PROGRESS }, - installed: undefined, + stateInfo: { + state: 'restoring', + installingInfo: { + newManifest: Mock.LocalPkgs[id].stateInfo.manifest!, + progress: PROGRESS, + }, + }, }, } }) @@ -746,84 +860,62 @@ export class MockApiService extends ApiService { return null } - async executePackageAction( - params: RR.ExecutePackageActionReq, - ): Promise { - await pauseFor(2000) - return Mock.ActionResponse - } - async startPackage(params: RR.StartPackageReq): Promise { - const path = `/package-data/${params.id}/installed/status/main` + const path = `/packageData/${params.id}/status` await pauseFor(2000) setTimeout(async () => { - const patch2 = [ - { - op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Running, - }, - { - op: PatchOp.REPLACE, - path: path + '/started', - value: new Date().toISOString(), - }, - ] - this.mockRevision(patch2) - - const patch3 = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: path + '/health', + path, value: { - 'ephemeral-health-check': { - result: 'starting', - }, - 'unnecessary-health-check': { - result: 'disabled', + main: 'running', + started: new Date().toISOString(), + health: { + 'ephemeral-health-check': { + name: 'Ephemeral Health Check', + result: 'starting', + message: null, + }, + 'unnecessary-health-check': { + name: 'Unnecessary Health Check', + result: 'disabled', + message: 'Custom disabled message', + }, + 'chain-state': { + name: 'Chain State', + result: 'loading', + message: 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + name: 'P2P Interface', + result: 'success', + message: null, + }, + 'rpc-interface': { + name: 'RPC Interface', + result: 'failure', + message: 'Custom failure message', + }, }, }, }, ] - this.mockRevision(patch3) - - await pauseFor(2000) - - const patch4 = [ - { - op: PatchOp.REPLACE, - path: path + '/health', - value: { - 'ephemeral-health-check': { - result: 'starting', - }, - 'unnecessary-health-check': { - result: 'disabled', - }, - 'chain-state': { - result: 'loading', - message: 'Bitcoin is syncing from genesis', - }, - 'p2p-interface': { - result: 'success', - }, - 'rpc-interface': { - result: 'failure', - error: 'RPC interface unreachable.', - }, - }, - }, - ] - this.mockRevision(patch4) + this.mockRevision(patch2) }, 2000) - const originalPatch = [ + const originalPatch: ReplaceOperation< + T.MainStatus & { main: 'starting' } + >[] = [ { op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Starting, + path, + value: { + main: 'starting', + health: {}, + }, }, ] @@ -835,74 +927,57 @@ export class MockApiService extends ApiService { async restartPackage( params: RR.RestartPackageReq, ): Promise { - // first enact stop await pauseFor(2000) - const path = `/package-data/${params.id}/installed/status/main` + const path = `/packageData/${params.id}/status` setTimeout(async () => { - const patch2: Operation[] = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Starting, - }, - { - op: PatchOp.ADD, - path: path + '/restarting', - value: true, - }, - ] - this.mockRevision(patch2) - - await pauseFor(2000) - - const patch3: Operation[] = [ - { - op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Running, - }, - { - op: PatchOp.REMOVE, - path: path + '/restarting', - }, - { - op: PatchOp.REPLACE, - path: path + '/health', + path, value: { - 'ephemeral-health-check': { - result: 'starting', - }, - 'unnecessary-health-check': { - result: 'disabled', - }, - 'chain-state': { - result: 'loading', - message: 'Bitcoin is syncing from genesis', - }, - 'p2p-interface': { - result: 'success', - }, - 'rpc-interface': { - result: 'failure', - error: 'RPC interface unreachable.', + main: 'running', + started: new Date().toISOString(), + health: { + 'ephemeral-health-check': { + name: 'Ephemeral Health Check', + result: 'starting', + message: null, + }, + 'unnecessary-health-check': { + name: 'Unnecessary Health Check', + result: 'disabled', + message: 'Custom disabled message', + }, + 'chain-state': { + name: 'Chain State', + result: 'loading', + message: 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + name: 'P2P Interface', + result: 'success', + message: null, + }, + 'rpc-interface': { + name: 'RPC Interface', + result: 'failure', + message: 'Custom failure message', + }, }, }, - } as any, + }, ] - this.mockRevision(patch3) + this.mockRevision(patch2) }, this.revertTime) - const patch = [ - { - op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Restarting, - }, + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: path + '/health', - value: {}, + path, + value: { + main: 'restarting', + }, }, ] @@ -913,29 +988,24 @@ export class MockApiService extends ApiService { async stopPackage(params: RR.StopPackageReq): Promise { await pauseFor(2000) - const path = `/package-data/${params.id}/installed/status/main` + const path = `/packageData/${params.id}/status` setTimeout(() => { - const patch2 = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Stopped, + path: path, + value: { main: 'stopped' }, }, ] this.mockRevision(patch2) }, this.revertTime) - const patch = [ + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Stopping, - }, - { - op: PatchOp.REPLACE, - path: path + '/health', - value: {}, + path: path, + value: { main: 'stopping' }, }, ] @@ -944,6 +1014,12 @@ export class MockApiService extends ApiService { return null } + async rebuildPackage( + params: RR.RebuildPackageReq, + ): Promise { + return this.restartPackage(params) + } + async uninstallPackage( params: RR.UninstallPackageReq, ): Promise { @@ -953,17 +1029,17 @@ export class MockApiService extends ApiService { const patch2: RemoveOperation[] = [ { op: PatchOp.REMOVE, - path: `/package-data/${params.id}`, + path: `/packageData/${params.id}`, }, ] this.mockRevision(patch2) }, this.revertTime) - const patch = [ + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/state`, - value: PackageState.Removing, + path: `/packageData/${params.id}/stateInfo/state`, + value: 'removing', }, ] @@ -972,74 +1048,444 @@ export class MockApiService extends ApiService { return null } - async dryConfigureDependency( - params: RR.DryConfigureDependencyReq, - ): Promise { + async sideloadPackage(): Promise { await pauseFor(2000) return { - 'old-config': Mock.MockConfig, - 'new-config': Mock.MockDependencyConfig, - spec: Mock.ConfigSpec, + upload: 'sideload-upload-guid', // no significance, randomly generated + progress: 'sideload-progress-guid', // no significance, randomly generated } } - async sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise { + async initAcme(params: RR.InitAcmeReq): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.ADD, + path: `/serverInfo/acme`, + value: { + [toAcmeUrl(params.provider)]: { contact: params.contact }, + }, + }, + ] + this.mockRevision(patch) + + return null + } + + async removeAcme(params: RR.RemoveAcmeReq): Promise { + await pauseFor(2000) + + const regex = new RegExp('/', 'g') + + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/serverInfo/acme/${params.provider.replace(regex, '~1')}`, + }, + ] + this.mockRevision(patch) + + return null + } + + async addTorKey(params: RR.AddTorKeyReq): Promise { + await pauseFor(2000) + return 'vanityabcdefghijklmnop' + } + + async generateTorKey(params: RR.GenerateTorKeyReq): Promise { + await pauseFor(2000) + return 'abcdefghijklmnopqrstuv' + } + + async serverBindingSetPubic( + params: RR.PkgBindingSetPublicReq, + ): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.REPLACE, + path: `/serverInfo/host/bindings/${params.internalPort}/net/public`, + value: params.public, + }, + ] + this.mockRevision(patch) + + return null + } + + async serverAddOnion(params: RR.ServerAddOnionReq): Promise { + await pauseFor(2000) + + const patch: Operation[] = [ + { + op: PatchOp.ADD, + path: `/serverInfo/host/onions/0`, + value: params.onion, + }, + { + op: PatchOp.ADD, + path: `/serverInfo/host/hostnameInfo/80/0`, + value: { + kind: 'onion', + hostname: { + port: 80, + sslPort: 443, + value: params.onion, + }, + }, + }, + ] + this.mockRevision(patch) + + return null + } + + async serverRemoveOnion( + params: RR.ServerRemoveOnionReq, + ): Promise { + await pauseFor(2000) + + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/serverInfo/host/onions/0`, + }, + { + op: PatchOp.REMOVE, + path: `/serverInfo/host/hostnameInfo/80/-1`, + }, + ] + this.mockRevision(patch) + + return null + } + + async serverAddDomain(params: RR.PkgAddDomainReq): Promise { + await pauseFor(2000) + + const patch: Operation[] = [ + { + op: PatchOp.ADD, + path: `/serverInfo/host/domains`, + value: { + [params.domain]: { public: !params.private, acme: params.acme }, + }, + }, + { + op: PatchOp.ADD, + path: `/serverInfo/host/hostnameInfo/80/0`, + value: { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'domain', + domain: params.domain, + subdomain: null, + port: null, + sslPort: 443, + }, + }, + }, + ] + this.mockRevision(patch) + + return null + } + + async serverRemoveDomain( + params: RR.PkgRemoveDomainReq, + ): Promise { + await pauseFor(2000) + + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/serverInfo/host/domains/${params.domain}`, + }, + { + op: PatchOp.REMOVE, + path: `/serverInfo/host/hostnameInfo/80/0`, + }, + ] + this.mockRevision(patch) + + return null + } + + async pkgBindingSetPubic( + params: RR.PkgBindingSetPublicReq, + ): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.REPLACE, + path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`, + value: params.public, + }, + ] + this.mockRevision(patch) + + return null + } + + async pkgAddOnion(params: RR.PkgAddOnionReq): Promise { + await pauseFor(2000) + + const patch: Operation[] = [ + { + op: PatchOp.ADD, + path: `/packageData/${params.package}/hosts/${params.host}/onions/0`, + value: params.onion, + }, + { + op: PatchOp.ADD, + path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + value: { + kind: 'onion', + hostname: { + port: 80, + sslPort: 443, + value: params.onion, + }, + }, + }, + ] + this.mockRevision(patch) + + return null + } + + async pkgRemoveOnion( + params: RR.PkgRemoveOnionReq, + ): Promise { + await pauseFor(2000) + + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/packageData/${params.package}/hosts/${params.host}/onions/0`, + }, + { + op: PatchOp.REMOVE, + path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + }, + ] + this.mockRevision(patch) + + return null + } + + async pkgAddDomain(params: RR.PkgAddDomainReq): Promise { await pauseFor(2000) - return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated + + const patch: Operation[] = [ + { + op: PatchOp.ADD, + path: `/packageData/${params.package}/hosts/${params.host}/domains`, + value: { + [params.domain]: { public: !params.private, acme: params.acme }, + }, + }, + { + op: PatchOp.ADD, + path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + value: { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'domain', + domain: params.domain, + subdomain: null, + port: null, + sslPort: 443, + }, + }, + }, + ] + this.mockRevision(patch) + + return null } - private async updateProgress(id: string): Promise { - const progress = { ...PROGRESS } - const phases = [ - { progress: 'downloaded', completion: 'download-complete' }, - { progress: 'validated', completion: 'validation-complete' }, - { progress: 'unpacked', completion: 'unpack-complete' }, - ] as const + async pkgRemoveDomain( + params: RR.PkgRemoveDomainReq, + ): Promise { + await pauseFor(2000) - for (let phase of phases) { - let i = progress[phase.progress] - const size = progress?.size || 0 - while (i < size) { - await pauseFor(250) - i = Math.min(i + 5, size) - progress[phase.progress] = i + const patch: RemoveOperation[] = [ + { + op: PatchOp.REMOVE, + path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`, + }, + { + op: PatchOp.REMOVE, + path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + }, + ] + this.mockRevision(patch) - if (i === progress.size) { - progress[phase.completion] = true + return null + } + + private async initProgress(): Promise { + const progress = JSON.parse(JSON.stringify(PROGRESS)) + + for (let [i, phase] of progress.phases.entries()) { + if ( + !phase.progress || + typeof phase.progress !== 'object' || + !phase.progress.total + ) { + await pauseFor(2000) + + progress.phases[i].progress = true + + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length + progress.overall.done += step } + } else { + const step = phase.progress.total / 4 - const patch = [ + while (phase.progress.done < phase.progress.total) { + await pauseFor(200) + + phase.progress.done += step + + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length / 4 + + progress.overall.done += step + } + + if (phase.progress.done === phase.progress.total) { + await pauseFor(250) + + progress.phases[i].progress = true + } + } + } + } + return progress + } + + private async installProgress(id: string): Promise { + const progress = JSON.parse(JSON.stringify(PROGRESS)) + + for (let [i, phase] of progress.phases.entries()) { + if (!phase.progress || phase.progress === true || !phase.progress.total) { + await pauseFor(2000) + + const patches: Operation[] = [ { op: PatchOp.REPLACE, - path: `/package-data/${id}/install-progress`, - value: { ...progress }, + path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress`, + value: true, }, ] - this.mockRevision(patch) + + // overall + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length + + progress.overall.done += step + + patches.push({ + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo/installingInfo/progress/overall/done`, + value: progress.overall.done, + }) + } + + this.mockRevision(patches) + } else { + const step = phase.progress.total / 4 + + while (phase.progress.done < phase.progress.total) { + await pauseFor(500) + + phase.progress.done += step + + const patches: Operation[] = [ + { + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress/done`, + value: phase.progress.done, + }, + ] + + // overall + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length / 4 + + progress.overall.done += step + + patches.push({ + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo/installingInfo/progress/overall/done`, + value: progress.overall.done, + }) + } + + this.mockRevision(patches) + + if (phase.progress.done === phase.progress.total) { + await pauseFor(250) + this.mockRevision([ + { + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress`, + value: true, + }, + ]) + } + } } } - setTimeout(() => { - const patch2: Operation[] = [ - { - op: PatchOp.REPLACE, - path: `/package-data/${id}/state`, - value: PackageState.Installed, - }, - { - op: PatchOp.ADD, - path: `/package-data/${id}/installed`, - value: { ...Mock.LocalPkgs[id].installed }, - }, - { - op: PatchOp.REMOVE, - path: `/package-data/${id}/install-progress`, + await pauseFor(1000) + this.mockRevision([ + { + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo/installingInfo/progress/overall`, + value: true, + }, + ]) + + await pauseFor(1000) + const patch2: Operation[] = [ + { + op: PatchOp.REPLACE, + path: `/packageData/${id}/stateInfo`, + value: { + state: 'installed', + manifest: Mock.LocalPkgs[id].stateInfo.manifest, }, - ] - this.mockRevision(patch2) - }, 1000) + }, + ] + this.mockRevision(patch2) } private async updateOSProgress() { @@ -1049,7 +1495,7 @@ export class MockApiService extends ApiService { const patch0 = [ { op: PatchOp.REPLACE, - path: `/server-info/status-info/update-progress/size`, + path: `/serverInfo/statusInfo/updateProgress/size`, value: size, }, ] @@ -1061,7 +1507,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: `/server-info/status-info/update-progress/downloaded`, + path: `/serverInfo/statusInfo/updateProgress/downloaded`, value: downloaded, }, ] @@ -1071,22 +1517,22 @@ export class MockApiService extends ApiService { const patch2 = [ { op: PatchOp.REPLACE, - path: `/server-info/status-info/update-progress/downloaded`, + path: `/serverInfo/statusInfo/updateProgress/downloaded`, value: size, }, ] this.mockRevision(patch2) setTimeout(async () => { - const patch3: Operation[] = [ + const patch3: Operation[] = [ { op: PatchOp.REPLACE, - path: '/server-info/status', - value: ServerStatus.Updated, + path: '/serverInfo/statusInfo/updated', + value: true, }, { op: PatchOp.REMOVE, - path: '/server-info/status-info/update-progress', + path: '/serverInfo/statusInfo/updateProgress', }, ] this.mockRevision(patch3) @@ -1095,8 +1541,8 @@ export class MockApiService extends ApiService { const patch4 = [ { op: PatchOp.REPLACE, - path: '/server-info/status', - value: ServerStatus.Running, + path: '/serverInfo/status', + value: 'running', }, ] this.mockRevision(patch4) @@ -1105,7 +1551,7 @@ export class MockApiService extends ApiService { const patch6 = [ { op: PatchOp.REPLACE, - path: '/server-info/status-info', + path: '/serverInfo/statusInfo', value: Mock.ServerUpdated, }, ] @@ -1114,10 +1560,6 @@ export class MockApiService extends ApiService { } private async mockRevision(patch: Operation[]): Promise { - if (!this.sequence) { - const { sequence } = this.bootstrapper.init() - this.sequence = sequence - } const revision = { id: ++this.sequence, patch, diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 1dc7abd66..9a80967a1 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -1,17 +1,12 @@ -import { - DataModel, - DockerIoFormat, - HealthResult, - PackageMainStatus, - PackageState, -} from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { Mock } from './api.fixures' import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets' +import { knownACME } from 'src/app/util/acme' +const version = require('../../../../../../package.json').version export const mockPatchData: DataModel = { ui: { name: `Matt's Server`, - 'ack-welcome': '1.0.0', theme: 'Dark', widgets: BUILT_IN_WIDGETS.filter( ({ id }) => @@ -21,8 +16,8 @@ export const mockPatchData: DataModel = { id === 'metrics', ), marketplace: { - 'selected-url': 'https://registry.start9.com/', - 'known-hosts': { + selectedUrl: 'https://registry.start9.com/', + knownHosts: { 'https://registry.start9.com/': { name: 'Start9 Registry', }, @@ -32,650 +27,617 @@ export const mockPatchData: DataModel = { }, }, }, - dev: {}, gaming: { snake: { - 'high-score': 0, + highScore: 0, }, }, - 'ack-instructions': {}, + ackInstructions: {}, }, - 'server-info': { + serverInfo: { + arch: 'x86_64', id: 'abcdefgh', - version: '0.3.5.1', - 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), - 'lan-address': 'https://adjective-noun.local', - 'tor-address': 'https://myveryownspecialtoraddress.onion', - 'ip-info': { + version, + lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(), + networkInterfaces: { eth0: { - ipv4: '10.0.0.1', - ipv6: null, + public: false, + ipInfo: { + scopeId: 1, + deviceType: 'ethernet', + subnets: ['10.0.0.2/24'], + wanIp: null, + ntpServers: [], + }, }, wlan0: { - ipv4: '10.0.90.12', - ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD', + public: false, + ipInfo: { + scopeId: 2, + deviceType: 'wireless', + subnets: [ + '10.0.90.12/24', + 'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64', + ], + wanIp: null, + ntpServers: [], + }, + }, + }, + acme: { + [knownACME[0].url]: { + contact: ['mailto:support@start9.com'], }, }, - 'last-wifi-region': null, - 'unread-notification-count': 4, + unreadNotificationCount: 4, // password is asdfasdf - 'password-hash': + passwordHash: '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'eos-version-compat': '>=0.3.0 <=0.3.0.1', - 'status-info': { - 'backup-progress': null, + packageVersionCompat: '>=0.3.0 <=0.3.6', + postInitMigrationTodos: [], + statusInfo: { + backupProgress: null, updated: false, - 'update-progress': null, + updateProgress: null, restarting: false, - 'shutting-down': false, + shuttingDown: false, }, hostname: 'random-words', - pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', - 'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', - 'ntp-synced': false, - zram: false, - platform: 'x86_64-nonfree', - }, - 'package-data': { - bitcoind: { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/bitcoind/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/bitcoind.svg', - instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md', - }, - manifest: { - id: 'bitcoind', - title: 'Bitcoin Core', - version: '0.20.0', - 'git-hash': 'abcdefgh', - description: { - short: 'A Bitcoin full node by Bitcoin Core.', - long: 'Bitcoin is a decentralized consensus protocol and settlement network.', - }, - 'release-notes': 'Taproot, Schnorr, and more.', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', - 'upstream-repo': 'https://github.com/bitcoin/bitcoin', - 'support-site': 'https://bitcoin.org', - 'marketing-site': 'https://bitcoin.org', - 'donation-url': 'https://start9.com', - alerts: { - install: 'Bitcoin can take over a week to sync.', - uninstall: - 'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.', - restore: null, - start: 'Starting Bitcoin is good for your health.', - stop: null, - }, - main: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': '.49m', - }, - 'health-checks': { - 'chain-state': { - name: 'Chain State', + host: { + bindings: { + 80: { + enabled: true, + net: { + assignedPort: null, + assignedSslPort: 443, + public: false, }, - 'ephemeral-health-check': { - name: 'Ephemeral Health Check', + options: { + preferredExternalPort: 80, + addSsl: { + preferredExternalPort: 443, + alpn: { specified: ['http/1.1', 'h2'] }, + }, + secure: null, }, - 'p2p-interface': { - name: 'P2P Interface', - 'success-message': 'the health check ran succesfully', + }, + }, + domains: {}, + onions: ['myveryownspecialtoraddress'], + hostnameInfo: { + 80: [ + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 443, + }, }, - 'rpc-interface': { - name: 'RPC Interface', + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 443, + }, }, - 'unnecessary-health-check': { - name: 'Unneccessary Health Check', + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.1', + port: null, + sslPort: 443, + }, }, - } as any, - config: { - get: {}, - set: {}, - } as any, - volumes: {}, - 'min-os-version': '0.2.12', - interfaces: { - ui: { - name: 'Node Visualizer', - description: - 'Web application for viewing information about your node and the Bitcoin network.', - ui: true, - 'tor-config': { - 'port-mapping': {}, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 443, }, - 'lan-config': {}, - protocols: [], }, - rpc: { - name: 'RPC', - description: - 'Used by wallets to interact with your Bitcoin Core node.', - ui: false, - 'tor-config': { - 'port-mapping': {}, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', + scopeId: 2, + port: null, + sslPort: 443, }, - 'lan-config': {}, - protocols: [], }, - p2p: { - name: 'P2P', - description: - 'Used by other Bitcoin nodes to communicate and interact with your node.', - ui: false, - 'tor-config': { - 'port-mapping': {}, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', + scopeId: 3, + port: null, + sslPort: 443, }, - 'lan-config': {}, - protocols: [], }, + { + kind: 'onion', + hostname: { + value: 'myveryownspecialtoraddress.onion', + port: 80, + sslPort: 443, + }, + }, + ], + }, + }, + pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', + caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', + ntpSynced: false, + platform: 'x86_64-nonfree', + zram: true, + governor: 'performance', + smtp: null, + wifi: { + interface: 'wlan0', + ssids: [], + selected: null, + lastRegion: null, + }, + ram: 8 * 1024 * 1024 * 1024, + devices: [], + }, + packageData: { + bitcoind: { + stateInfo: { + state: 'installed', + manifest: { + ...Mock.MockManifestBitcoind, + version: '0.20.0:0', }, - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, + }, + dataVersion: '0.20.0:0', + icon: '/assets/img/service-icons/bitcoind.svg', + lastBackup: null, + status: { + main: 'stopped', + }, + // status: { + // main: 'error', + // message: 'Bitcoin is erroring out', + // debug: 'This is a complete stack trace for bitcoin', + // onRebuild: 'start', + // }, + actions: { + config: { + name: 'Set Config', + description: 'edit bitcoin.conf', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + rpc: { + name: 'Set RPC', + description: 'Create RPC Credentials', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, + }, + properties: { + name: 'View Properties', + description: 'view important information about Bitcoin', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: false, + group: null, + }, + test: { + name: 'Do Another Thing', + description: + 'An example of an action that shows a warning and takes no input', + warning: 'careful running this action', + visibility: { disabled: 'This is temporarily disabled' }, + allowedStatuses: 'only-running', + hasInput: false, + group: null, + }, + }, + serviceInterfaces: { + ui: { + id: 'ui', + masked: false, + name: 'Web UI', + description: + 'A launchable web app for you to interact with your Bitcoin node', + type: 'ui', + addressInfo: { + username: null, + hostId: 'abcdefg', + internalPort: 80, + scheme: 'http', + sslScheme: 'https', + suffix: '', }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, + }, + rpc: { + id: 'rpc', + masked: false, + name: 'RPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'bcdefgh', + internalPort: 8332, + scheme: 'http', + sslScheme: 'https', + suffix: '', }, }, - migrations: null, - actions: { - resync: { - name: 'Resync Blockchain', - description: - 'Use this to resync the Bitcoin blockchain from genesis', - warning: 'This will take a couple of days.', - 'allowed-statuses': [ - PackageMainStatus.Running, - PackageMainStatus.Stopped, - ], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': { - reason: { - type: 'string', - name: 'Re-sync Reason', - description: - 'Your reason for re-syncing. Why are you doing this?', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', + p2p: { + id: 'p2p', + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'cdefghi', + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, + suffix: '', + }, + }, + }, + currentDependencies: {}, + hosts: { + abcdefg: { + bindings: { + 80: { + enabled: true, + net: { + assignedPort: 80, + assignedSslPort: 443, + public: false, }, - name: { - type: 'string', - name: 'Your Name', - description: 'Tell the class your name.', - nullable: true, - masked: false, - copyable: false, - warning: 'You may loose all your money by providing your name.', + options: { + addSsl: null, + preferredExternalPort: 443, + secure: { ssl: true }, }, - notifications: { - name: 'Notification Preferences', - type: 'list', - subtype: 'enum', - description: 'how you want to be notified', - range: '[1,3]', - default: ['email'], - spec: { - 'value-names': { - email: 'Email', - text: 'Text', - call: 'Call', - push: 'Push', - webhook: 'Webhook', - }, - values: ['email', 'text', 'call', 'push', 'webhook'], + }, + }, + onions: [], + domains: {}, + hostnameInfo: { + 80: [ + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, }, }, - 'days-ago': { - type: 'number', - name: 'Days Ago', - description: 'Number of days to re-sync.', - nullable: false, - default: 100, - range: '[0, 9999]', - integral: true, - }, - 'top-speed': { - type: 'number', - name: 'Top Speed', - description: 'The fastest you can possibly run.', - nullable: false, - range: '[-1000, 1000]', - integral: false, - units: 'm/s', - }, - testnet: { - name: 'Testnet', - type: 'boolean', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: false, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, }, - randomEnum: { - name: 'Random Enum', - type: 'enum', - 'value-names': { - null: 'Null', - good: 'Good', - bad: 'Bad', - ugly: 'Ugly', + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.1', + port: null, + sslPort: 1234, }, - default: 'null', - description: 'This is not even real.', - warning: 'Be careful changing this!', - values: ['null', 'good', 'bad', 'ugly'], }, - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - spec: { - name: { - type: 'string', - name: 'Name', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - email: { - type: 'string', - name: 'Email', - nullable: false, - masked: false, - copyable: true, - }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, }, }, - ips: { - name: 'Whitelist IPs', - type: 'list', - subtype: 'string', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - range: '[1,10]', - default: ['192.168.1.1'], - spec: { - pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$', - 'pattern-description': 'Must be a valid IP address', - masked: false, - copyable: false, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', + scopeId: 2, + port: null, + sslPort: 1234, }, }, - bitcoinNode: { - type: 'union', - default: 'internal', - tag: { - id: 'type', - 'variant-names': { - internal: 'Internal', - external: 'External', - }, - name: 'Bitcoin Node Settings', - description: 'The node settings', - warning: 'Careful changing this', + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', + scopeId: 3, + port: null, + sslPort: 1234, }, - variants: { - internal: { - 'lan-address': { - name: 'LAN Address', - type: 'pointer', - subtype: 'package', - target: 'lan-address', - 'package-id': 'bitcoind', - description: 'the lan address', - interface: '', - }, - 'friendly-name': { - name: 'Friendly Name', - type: 'string', - description: 'the lan address', - nullable: true, - masked: false, - copyable: false, - }, - }, - external: { - 'public-domain': { - name: 'Public Domain', - type: 'string', - description: 'the public address of the node', - nullable: false, - default: 'bitcoinnode.com', - pattern: '.*', - 'pattern-description': 'anything', - masked: false, - copyable: true, - }, - }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p.onion', + port: 80, + sslPort: 443, }, }, - }, + ], }, }, - dependencies: {}, - }, - installed: { - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.20.0', - }, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: '2021-06-14T20:49:17.774Z', - health: { - 'ephemeral-health-check': { - result: HealthResult.Starting, - }, - 'chain-state': { - result: HealthResult.Loading, - message: 'Bitcoin is syncing from genesis', + bcdefgh: { + bindings: { + 8332: { + enabled: true, + net: { + assignedPort: 8332, + assignedSslPort: null, + public: false, }, - 'p2p-interface': { - result: HealthResult.Success, - }, - 'rpc-interface': { - result: HealthResult.Failure, - error: 'RPC interface unreachable.', - }, - 'unnecessary-health-check': { - result: HealthResult.Disabled, + options: { + addSsl: null, + preferredExternalPort: 8332, + secure: { ssl: false }, }, }, }, - 'dependency-config-errors': {}, + onions: [], + domains: {}, + hostnameInfo: { + 8332: [], + }, }, - 'interface-addresses': { - ui: { - 'tor-address': 'bitcoind-ui-address.onion', - 'lan-address': 'bitcoind-ui-address.local', + cdefghi: { + bindings: { + 8333: { + enabled: true, + net: { + assignedPort: 8333, + assignedSslPort: null, + public: false, + }, + options: { + addSsl: null, + preferredExternalPort: 8333, + secure: { ssl: false }, + }, + }, }, - rpc: { - 'tor-address': 'bitcoind-rpc-address.onion', - 'lan-address': 'bitcoind-rpc-address.local', + onions: [], + domains: {}, + hostnameInfo: { + 8333: [], }, - p2p: { - 'tor-address': 'bitcoind-p2p-address.onion', - 'lan-address': 'bitcoind-p2p-address.local', + }, + }, + storeExposedDependents: [], + registry: 'https://registry.start9.com/', + developerKey: 'developer-key', + requestedActions: { + 'bitcoind-config': { + request: { + packageId: 'bitcoind', + actionId: 'config', + severity: 'critical', + reason: + 'You must run Config before starting Bitcoin for the first time', }, + active: true, }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], + 'bitcoind-properties': { + request: { + packageId: 'bitcoind', + actionId: 'properties', + severity: 'important', + reason: 'Check out all the info about your Bitcoin node', }, + active: true, }, - 'current-dependencies': {}, - 'dependency-info': {}, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, }, lnd: { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/lnd/0.11.1/LICENSE.md', - icon: '/assets/img/service-icons/lnd.png', - instructions: '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md', - }, - manifest: { - id: 'lnd', - title: 'Lightning Network Daemon', - version: '0.11.0', - description: { - short: 'A bolt spec compliant client.', - long: 'More info about LND. More info about LND. More info about LND.', - }, - 'release-notes': 'Dual funded channels!', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', - 'upstream-repo': 'https://github.com/lightningnetwork/lnd', - 'support-site': 'https://lightning.engineering/', - 'marketing-site': 'https://lightning.engineering/', - 'donation-url': null, - alerts: { - install: null, - uninstall: null, - restore: - 'If this is a duplicate instance of the same LND node, you may loose your funds.', - start: 'Starting LND is good for your health.', - stop: null, - }, - main: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': '0.5s', + stateInfo: { + state: 'installed', + manifest: { + ...Mock.MockManifestLnd, + version: '0.11.0:0.0.1', }, - 'health-checks': {}, + }, + dataVersion: '0.11.0:0.0.1', + icon: '/assets/img/service-icons/lnd.png', + lastBackup: null, + status: { + main: 'stopped', + }, + actions: { config: { - get: null, - set: null, + name: 'Config', + description: 'LND needs configuration before starting', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, }, - volumes: {}, - 'min-os-version': '0.2.12', - interfaces: { - rpc: { - name: 'RPC interface', - description: 'Good for connecting to your node at a distance.', - ui: true, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': { - '44': { - ssl: true, - mapping: 33, - }, - }, - protocols: [], - }, - grpc: { - name: 'GRPC', - description: 'Certain wallet use grpc.', - ui: false, - 'tor-config': { - 'port-mapping': {}, - }, - 'lan-config': { - '66': { - ssl: true, - mapping: 55, - }, - }, - protocols: [], - }, + connect: { + name: 'Connect', + description: 'View LND connection details', + warning: null, + visibility: 'enabled', + allowedStatuses: 'any', + hasInput: true, + group: null, }, - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, + }, + serviceInterfaces: { + grpc: { + id: 'grpc', + masked: false, + name: 'GRPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + internalPort: 10009, + scheme: null, + sslScheme: 'grpc', + suffix: '', }, }, - migrations: null, - actions: { - resync: { - name: 'Resync Network Graph', - description: 'Your node will resync its network graph.', - warning: 'This will take a couple hours.', - 'allowed-statuses': [PackageMainStatus.Running], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': null, + lndconnect: { + id: 'lndconnect', + masked: true, + name: 'LND Connect', + description: + 'Used by client wallets adhering to LND Connect protocol to connect to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + internalPort: 10009, + scheme: null, + sslScheme: 'lndconnect', + suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, }, - dependencies: { - bitcoind: { - version: '=0.21.0', - description: 'LND needs bitcoin to live.', - requirement: { - type: 'opt-out', - how: 'You can use an external node from your server if you prefer.', - }, - config: null, - }, - 'btc-rpc-proxy': { - version: '>=0.2.2', - description: - 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', - requirement: { - type: 'opt-in', - how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, - }, - config: null, + p2p: { + id: 'p2p', + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'rstuvw', + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, + suffix: '', }, }, }, - installed: { - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.0', + currentDependencies: { + bitcoind: { + title: 'Bitcoin Core', + icon: 'assets/img/service-icons/bitcoind.svg', + kind: 'running', + versionRange: '>=26.0.0', + healthChecks: [], }, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-config-errors': { - 'btc-rpc-proxy': 'This is a config unsatisfied error', - }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: 'assets/img/service-icons/btc-rpc-proxy.png', + kind: 'running', + versionRange: '>2.0.0', + healthChecks: [], }, - 'interface-addresses': { - rpc: { - 'tor-address': 'lnd-rpc-address.onion', - 'lan-address': 'lnd-rpc-address.local', - }, - grpc: { - 'tor-address': 'lnd-grpc-address.onion', - 'lan-address': 'lnd-grpc-address.local', + }, + hosts: {}, + storeExposedDependents: [], + registry: 'https://registry.start9.com/', + developerKey: 'developer-key', + requestedActions: { + config: { + active: true, + request: { + packageId: 'lnd', + actionId: 'config', + severity: 'critical', + reason: 'LND needs configuration before starting', }, }, - 'system-pointers': [], - 'current-dependents': {}, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], - }, - 'btc-rpc-proxy': { - pointers: [], - 'health-checks': [], + connect: { + active: true, + request: { + packageId: 'lnd', + actionId: 'connect', + severity: 'important', + reason: 'View LND connection details', }, }, - 'dependency-info': { - bitcoind: { - title: 'Bitcoin Core', - icon: 'assets/img/service-icons/bitcoind.svg', + 'bitcoind/config': { + active: true, + request: { + packageId: 'bitcoind', + actionId: 'config', + severity: 'critical', + reason: 'LND likes BTC a certain way', + input: { + kind: 'partial', + value: { + color: '#ffffff', + testnet: false, + }, + }, }, - 'btc-rpc-proxy': { - title: 'Bitcoin Proxy', - icon: 'assets/img/service-icons/btc-rpc-proxy.png', + }, + 'bitcoind/rpc': { + active: true, + request: { + packageId: 'bitcoind', + actionId: 'rpc', + severity: 'important', + reason: `LND want's its own RPC credentials`, + input: { + kind: 'partial', + value: { + rpcsettings: { + rpcuser: 'lnd', + }, + }, + }, }, }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, }, }, diff --git a/web/projects/ui/src/app/services/auth.service.ts b/web/projects/ui/src/app/services/auth.service.ts index 5d755aa98..2db008d8e 100644 --- a/web/projects/ui/src/app/services/auth.service.ts +++ b/web/projects/ui/src/app/services/auth.service.ts @@ -12,7 +12,7 @@ export enum AuthState { providedIn: 'root', }) export class AuthService { - private readonly LOGGED_IN_KEY = 'loggedInKey' + private readonly LOGGED_IN_KEY = 'loggedIn' private readonly authState$ = new ReplaySubject(1) readonly isVerified$ = this.authState$.pipe( @@ -27,9 +27,9 @@ export class AuthService { ) {} init(): void { - const loggedIn = this.storage.get(this.LOGGED_IN_KEY) + const loggedIn = this.storage.get(this.LOGGED_IN_KEY) if (loggedIn) { - this.setVerified() + this.authState$.next(AuthState.VERIFIED) } else { this.setUnverified() } diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index f8fef3d60..79455b411 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -1,12 +1,8 @@ import { DOCUMENT } from '@angular/common' import { Inject, Injectable } from '@angular/core' import { WorkspaceConfig } from '@start9labs/shared' -import { - InterfaceDef, - PackageDataEntry, - PackageMainStatus, - PackageState, -} from 'src/app/services/patch-db/data-model' +import { T, utils } from '@start9labs/start-sdk' +import { PackageDataEntry } from './patch-db/data-model' const { gitHash, @@ -32,8 +28,7 @@ export class ConfigService { api = api marketplace = marketplace skipStartupAlerts = useMocks && mocks.skipStartupAlerts - isConsulate = (window as any)['platform'] === 'ios' - supportsWebSockets = !!window.WebSocket || this.isConsulate + supportsWebSockets = !!window.WebSocket isTor(): boolean { return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion') @@ -45,101 +40,229 @@ export class ConfigService { : this.hostname.endsWith('.local') } - isTorHttp(): boolean { - return this.isTor() && !this.isHttps() + isLocalhost(): boolean { + return useMocks + ? mocks.maskAs === 'localhost' + : this.hostname === 'localhost' || this.hostname === '127.0.0.1' + } + + isIpv4(): boolean { + return useMocks + ? mocks.maskAs === 'ipv4' + : new RegExp(utils.Patterns.ipv4.regex).test(this.hostname) + } + + isLanIpv4(): boolean { + return useMocks + ? mocks.maskAs === 'ipv4' + : new RegExp(utils.Patterns.ipv4.regex).test(this.hostname) && + (this.hostname.startsWith('192.168.') || + this.hostname.startsWith('10.') || + (this.hostname.startsWith('172.') && + !![this.hostname.split('.').map(Number)[1]].filter( + n => n >= 16 && n < 32, + ).length)) + } + + isIpv6(): boolean { + return useMocks + ? mocks.maskAs === 'ipv6' + : new RegExp(utils.Patterns.ipv6.regex).test(this.hostname) } isLanHttp(): boolean { return !this.isTor() && !this.isLocalhost() && !this.isHttps() } + isClearnet(): boolean { + return useMocks + ? mocks.maskAs === 'clearnet' + : this.isHttps() && + !this.isTor() && + !this.isLocal() && + !this.isLocalhost() && + !this.isLanIpv4() && + !this.isIpv6() + } + + isHttps(): boolean { + return useMocks ? mocks.maskAsHttps : this.protocol === 'https:' + } + isSecure(): boolean { return window.isSecureContext || this.isTor() } isLaunchable( - state: PackageState, - status: PackageMainStatus, - interfaces: Record, + state: T.PackageState['state'], + status: T.MainStatus['main'], ): boolean { - return ( - state === PackageState.Installed && - status === PackageMainStatus.Running && - hasUi(interfaces) - ) + return state === 'installed' && status === 'running' } - launchableURL(pkg: PackageDataEntry): string { - if (!this.isTor() && hasLocalUi(pkg.manifest.interfaces)) { - return `https://${lanUiAddress(pkg)}` - } else { - return `http://${torUiAddress(pkg)}` - } - } + /** ${scheme}://${username}@${host}:${externalPort}${suffix} */ + launchableAddress( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: T.Hosts, + ): string { + const ui = Object.values(interfaces).find( + i => + i.type === 'ui' && + (i.addressInfo.scheme === 'http' || + i.addressInfo.sslScheme === 'https'), + ) - getHost(): string { - return this.host - } + if (!ui) return '' - private isLocalhost(): boolean { - return useMocks - ? mocks.maskAs === 'localhost' - : this.hostname === 'localhost' - } + const host = hosts[ui.addressInfo.hostId] - private isHttps(): boolean { - return useMocks ? mocks.maskAsHttps : this.protocol === 'https:' - } -} + if (!host) return '' -export function hasTorUi(interfaces: Record): boolean { - const int = getUiInterfaceValue(interfaces) - return !!int?.['tor-config'] -} + let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort] + hostnameInfo = hostnameInfo.filter( + h => + this.isLocalhost() || + h.kind !== 'ip' || + h.hostname.kind !== 'ipv6' || + !h.hostname.value.startsWith('fe80::'), + ) + if (this.isLocalhost()) { + const local = hostnameInfo.find( + h => h.kind === 'ip' && h.hostname.kind === 'local', + ) + if (local) { + hostnameInfo.unshift({ + kind: 'ip', + networkInterfaceId: 'lo', + public: false, + hostname: { + kind: 'local', + port: local.hostname.port, + sslPort: local.hostname.sslPort, + value: 'localhost', + }, + }) + } + } -export function hasLocalUi(interfaces: Record): boolean { - const int = getUiInterfaceValue(interfaces) - return !!int?.['lan-config'] -} + if (!hostnameInfo) return '' -export function torUiAddress({ - manifest, - installed, -}: PackageDataEntry): string { - const key = getUiInterfaceKey(manifest.interfaces) - return installed ? installed['interface-addresses'][key]['tor-address'] : '' -} + const addressInfo = ui.addressInfo + const username = addressInfo.username ? addressInfo.username + '@' : '' + const suffix = addressInfo.suffix || '' + const url = new URL(`https://${username}placeholder${suffix}`) + const use = (hostname: { + value: string + port: number | null + sslPort: number | null + }) => { + url.hostname = hostname.value + const useSsl = + hostname.port && hostname.sslPort ? this.isHttps() : !!hostname.sslPort + url.protocol = useSsl + ? `${addressInfo.sslScheme || 'https'}:` + : `${addressInfo.scheme || 'http'}:` + const port = useSsl ? hostname.sslPort : hostname.port + const omitPort = useSsl + ? ui.addressInfo.sslScheme === 'https' && port === 443 + : ui.addressInfo.scheme === 'http' && port === 80 + if (!omitPort && port) url.port = String(port) + } + const useFirst = ( + hostnames: ( + | { + value: string + port: number | null + sslPort: number | null + } + | undefined + )[], + ) => { + const first = hostnames.find(h => h) + if (first) { + use(first) + } + return !!first + } -export function lanUiAddress({ - manifest, - installed, -}: PackageDataEntry): string { - const key = getUiInterfaceKey(manifest.interfaces) - return installed ? installed['interface-addresses'][key]['lan-address'] : '' -} + const ipHostnames = hostnameInfo + .filter(h => h.kind === 'ip') + .map(h => h.hostname) as T.IpHostname[] + const domainHostname = ipHostnames + .filter(h => h.kind === 'domain') + .map(h => h as T.IpHostname & { kind: 'domain' }) + .map(h => ({ + value: h.domain, + sslPort: h.sslPort, + port: h.port, + }))[0] + const wanIpHostname = hostnameInfo + .filter(h => h.kind === 'ip' && h.public && h.hostname.kind !== 'domain') + .map(h => h.hostname as Exclude) + .map(h => ({ + value: h.value, + sslPort: h.sslPort, + port: h.port, + }))[0] + const onionHostname = hostnameInfo + .filter(h => h.kind === 'onion') + .map(h => h as T.HostnameInfo & { kind: 'onion' }) + .map(h => ({ + value: h.hostname.value, + sslPort: h.hostname.sslPort, + port: h.hostname.port, + }))[0] + const localHostname = ipHostnames + .filter(h => h.kind === 'local') + .map(h => h as T.IpHostname & { kind: 'local' }) + .map(h => ({ value: h.value, sslPort: h.sslPort, port: h.port }))[0] -export function hasUi(interfaces: Record): boolean { - return hasTorUi(interfaces) || hasLocalUi(interfaces) -} + if (this.isClearnet()) { + if ( + !useFirst([domainHostname, wanIpHostname, onionHostname, localHostname]) + ) { + return '' + } + } else if (this.isTor()) { + if ( + !useFirst([onionHostname, domainHostname, wanIpHostname, localHostname]) + ) { + return '' + } + } else if (this.isIpv6()) { + const ipv6Hostname = ipHostnames.find(h => h.kind === 'ipv6') as { + kind: 'ipv6' + value: string + scopeId: number + port: number | null + sslPort: number | null + } -export function removeProtocol(str: string): string { - if (str.startsWith('http://')) return str.slice(7) - if (str.startsWith('https://')) return str.slice(8) - return str -} + if (!useFirst([ipv6Hostname, localHostname])) { + return '' + } + } else { + // ipv4 or .local or localhost -export function removePort(str: string): string { - return str.split(':')[0] -} + if (!localHostname) return '' + + use({ + value: this.hostname, + port: localHostname.port, + sslPort: localHostname.sslPort, + }) + } + + return url.href + } -export function getUiInterfaceKey( - interfaces: Record, -): string { - return Object.keys(interfaces).find(key => interfaces[key].ui) || '' + getHost(): string { + return this.host + } } -export function getUiInterfaceValue( - interfaces: Record, -): InterfaceDef | null { - return Object.values(interfaces).find(i => i.ui) || null +export function hasUi( + interfaces: PackageDataEntry['serviceInterfaces'], +): boolean { + return Object.values(interfaces).some(iface => iface.type === 'ui') } diff --git a/web/projects/ui/src/app/services/connection.service.ts b/web/projects/ui/src/app/services/connection.service.ts index a45d5ec4c..7d3328503 100644 --- a/web/projects/ui/src/app/services/connection.service.ts +++ b/web/projects/ui/src/app/services/connection.service.ts @@ -1,25 +1,23 @@ -import { Injectable } from '@angular/core' -import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs' -import { distinctUntilChanged, map, startWith } from 'rxjs/operators' +import { inject, Injectable } from '@angular/core' +import { combineLatest, Observable, shareReplay } from 'rxjs' +import { distinctUntilChanged, map } from 'rxjs/operators' +import { NetworkService } from 'src/app/services/network.service' +import { StateService } from 'src/app/services/state.service' @Injectable({ providedIn: 'root', }) -export class ConnectionService { - readonly networkConnected$ = merge( - fromEvent(window, 'online'), - fromEvent(window, 'offline'), - ).pipe( - startWith(null), - map(() => navigator.onLine), - distinctUntilChanged(), - ) - readonly websocketConnected$ = new ReplaySubject(1) - readonly connected$ = combineLatest([ - this.networkConnected$, - this.websocketConnected$.pipe(distinctUntilChanged()), +export class ConnectionService extends Observable { + private readonly stream$ = combineLatest([ + inject(NetworkService), + inject(StateService).pipe(map(Boolean)), ]).pipe( map(([network, websocket]) => network && websocket), distinctUntilChanged(), + shareReplay(1), ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } } diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 4762f2df1..cd15ebff6 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { DataModel, - HealthCheckResult, - HealthResult, - InstalledPackageDataEntry, - PackageMainStatus, + InstalledState, + PackageDataEntry, } from './patch-db/data-model' import * as deepEqual from 'fast-deep-equal' +import { isInstalled } from '../util/get-package-data' +import { DependencyError } from './api/api.types' export type AllDependencyErrors = Record export type PkgDependencyErrors = Record @@ -18,7 +18,7 @@ export type PkgDependencyErrors = Record providedIn: 'root', }) export class DepErrorService { - readonly depErrors$ = this.patch.watch$('package-data').pipe( + readonly depErrors$ = this.patch.watch$('packageData').pipe( map(pkgs => Object.keys(pkgs) .map(id => ({ @@ -39,7 +39,7 @@ export class DepErrorService { ) constructor( - private readonly emver: Emver, + private readonly exver: Exver, private readonly patch: PatchDB, ) {} @@ -51,88 +51,87 @@ export class DepErrorService { } private getDepErrors( - pkgs: DataModel['package-data'], + pkgs: DataModel['packageData'], pkgId: string, outerErrors: AllDependencyErrors, ): PkgDependencyErrors { - const pkgInstalled = pkgs[pkgId].installed + const pkg = pkgs[pkgId] - if (!pkgInstalled) return {} + if (!isInstalled(pkg)) return {} return currentDeps(pkgs, pkgId).reduce( (innerErrors, depId): PkgDependencyErrors => ({ ...innerErrors, - [depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors), + [depId]: this.getDepError(pkgs, pkg, depId, outerErrors), }), {} as PkgDependencyErrors, ) } private getDepError( - pkgs: DataModel['package-data'], - pkgInstalled: InstalledPackageDataEntry, + pkgs: DataModel['packageData'], + pkg: PackageDataEntry, depId: string, outerErrors: AllDependencyErrors, ): DependencyError | null { - const depInstalled = pkgs[depId]?.installed + const dep = pkgs[depId] // not installed - if (!depInstalled) { + if (!dep || dep.stateInfo.state !== 'installed') { return { - type: DependencyErrorType.NotInstalled, + type: 'notInstalled', } } - const pkgManifest = pkgInstalled.manifest - const depManifest = depInstalled.manifest + const currentDep = pkg.currentDependencies[depId] + const depManifest = dep.stateInfo.manifest // incorrect version - if ( - !this.emver.satisfies( - depManifest.version, - pkgManifest.dependencies[depId].version, - ) - ) { - return { - type: DependencyErrorType.IncorrectVersion, - expected: pkgManifest.dependencies[depId].version, - received: depManifest.version, + if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) { + if ( + depManifest.satisfies.some( + v => !this.exver.satisfies(v, currentDep.versionRange), + ) + ) { + return { + type: 'incorrectVersion', + expected: currentDep.versionRange, + received: depManifest.version, + } } } - // invalid config + // action required if ( - Object.values(pkgInstalled.status['dependency-config-errors']).some( - err => !!err, + Object.values(pkg.requestedActions).some( + a => + a.active && + a.request.packageId === depId && + a.request.severity === 'critical', ) ) { return { - type: DependencyErrorType.ConfigUnsatisfied, + type: 'actionRequired', } } - const depStatus = depInstalled.status.main.status + const depStatus = dep.status.main // not running - if ( - depStatus !== PackageMainStatus.Running && - depStatus !== PackageMainStatus.Starting - ) { + if (depStatus !== 'running' && depStatus !== 'starting') { return { - type: DependencyErrorType.NotRunning, + type: 'notRunning', } } // health check failure - if (depStatus === PackageMainStatus.Running) { - for (let id of pkgInstalled['current-dependencies'][depId][ - 'health-checks' - ]) { - if ( - depInstalled.status.main.health[id]?.result !== HealthResult.Success - ) { + if (depStatus === 'running' && currentDep.kind === 'running') { + for (let id of currentDep.healthChecks) { + const check = dep.status.health[id] + if (check?.result !== 'success') { return { - type: DependencyErrorType.HealthChecksFailed, + type: 'healthChecksFailed', + check, } } } @@ -145,7 +144,7 @@ export class DepErrorService { if (transitiveError) { return { - type: DependencyErrorType.Transitive, + type: 'transitive', } } @@ -153,14 +152,14 @@ export class DepErrorService { } } -function currentDeps(pkgs: DataModel['package-data'], id: string): string[] { - return Object.keys( - pkgs[id]?.installed?.['current-dependencies'] || {}, - ).filter(depId => depId !== id) +function currentDeps(pkgs: DataModel['packageData'], id: string): string[] { + return Object.keys(pkgs[id]?.currentDependencies || {}).filter( + depId => depId !== id, + ) } function dependencyDepth( - pkgs: DataModel['package-data'], + pkgs: DataModel['packageData'], id: string, depth = 0, ): number { @@ -169,46 +168,3 @@ function dependencyDepth( depth, ) } - -export type DependencyError = - | DependencyErrorNotInstalled - | DependencyErrorNotRunning - | DependencyErrorIncorrectVersion - | DependencyErrorConfigUnsatisfied - | DependencyErrorHealthChecksFailed - | DependencyErrorTransitive - -export enum DependencyErrorType { - NotInstalled = 'notInstalled', - NotRunning = 'notRunning', - IncorrectVersion = 'incorrectVersion', - ConfigUnsatisfied = 'configUnsatisfied', - HealthChecksFailed = 'healthChecksFailed', - Transitive = 'transitive', -} - -export interface DependencyErrorNotInstalled { - type: DependencyErrorType.NotInstalled -} - -export interface DependencyErrorNotRunning { - type: DependencyErrorType.NotRunning -} - -export interface DependencyErrorIncorrectVersion { - type: DependencyErrorType.IncorrectVersion - expected: string // version range - received: string // version -} - -export interface DependencyErrorConfigUnsatisfied { - type: DependencyErrorType.ConfigUnsatisfied -} - -export interface DependencyErrorHealthChecksFailed { - type: DependencyErrorType.HealthChecksFailed -} - -export interface DependencyErrorTransitive { - type: DependencyErrorType.Transitive -} diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/eos.service.ts index 396c84223..5c1b2c5ee 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/eos.service.ts @@ -1,27 +1,27 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' import { BehaviorSubject, combineLatest } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' -import { MarketplaceEOS } from 'src/app/services/api/api.types' +import { OSUpdate } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' import { getServerInfo } from 'src/app/util/get-server-info' import { DataModel } from './patch-db/data-model' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', }) export class EOSService { - eos?: MarketplaceEOS + osUpdate?: OSUpdate updateAvailable$ = new BehaviorSubject(false) - readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe( - map(status => !!status['update-progress'] || status.updated), + readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe( + map(status => !!status.updateProgress || status.updated), distinctUntilChanged(), ) readonly backingUp$ = this.patch - .watch$('server-info', 'status-info', 'backup-progress') + .watch$('serverInfo', 'statusInfo', 'backupProgress') .pipe( map(obj => !!obj), distinctUntilChanged(), @@ -47,14 +47,15 @@ export class EOSService { constructor( private readonly api: ApiService, - private readonly emver: Emver, private readonly patch: PatchDB, ) {} async loadEos(): Promise { - const { version } = await getServerInfo(this.patch) - this.eos = await this.api.getEos() - const updateAvailable = this.emver.compare(this.eos.version, version) === 1 + const { version, id } = await getServerInfo(this.patch) + this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) + const updateAvailable = + Version.parse(this.osUpdate.version).compare(Version.parse(version)) === + 'greater' this.updateAvailable$.next(updateAvailable) } } diff --git a/web/projects/ui/src/app/services/form-dialog.service.ts b/web/projects/ui/src/app/services/form-dialog.service.ts new file mode 100644 index 000000000..69df946bb --- /dev/null +++ b/web/projects/ui/src/app/services/form-dialog.service.ts @@ -0,0 +1,41 @@ +import { inject, Injectable, Injector, Type } from '@angular/core' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TuiDialogFormService, TuiPromptData } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' + +const PROMPT: Partial> = { + label: 'Unsaved Changes', + data: { + content: 'You have unsaved changes. Are you sure you want to leave?', + yes: 'Leave', + no: 'Cancel', + }, +} + +@Injectable({ providedIn: 'root' }) +export class FormDialogService { + private readonly dialogs = inject(TuiDialogService) + private readonly formService = new TuiDialogFormService(this.dialogs) + private readonly prompt = this.formService.withPrompt(PROMPT) + private readonly injector = Injector.create({ + parent: inject(Injector), + providers: [ + { + provide: TuiDialogFormService, + useValue: this.formService, + }, + ], + }) + + open(component: Type, options: Partial> = {}) { + this.dialogs + .open(new PolymorpheusComponent(component, this.injector), { + closeable: this.prompt, + dismissible: this.prompt, + ...options, + }) + .subscribe({ + complete: () => this.formService.markAsPristine(), + }) + } +} diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index 66d368b50..04ee483a6 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -7,24 +7,7 @@ import { ValidatorFn, Validators, } from '@angular/forms' -import { - ConfigSpec, - isValueSpecListOf, - ListValueSpecNumber, - ListValueSpecObject, - ListValueSpecOf, - ListValueSpecString, - ListValueSpecUnion, - UniqueBy, - ValueSpec, - ValueSpecEnum, - ValueSpecList, - ValueSpecNumber, - ValueSpecObject, - ValueSpecString, - ValueSpecUnion, -} from 'src/app/pkg-config/config-types' -import { getDefaultString, Range } from '../pkg-config/config-utilities' +import { IST, utils } from '@start9labs/start-sdk' const Mustache = require('mustache') @Injectable({ @@ -34,212 +17,312 @@ export class FormService { constructor(private readonly formBuilder: UntypedFormBuilder) {} createForm( - spec: ConfigSpec, - current: { [key: string]: any } = {}, + spec: IST.InputSpec, + current: Record = {}, ): UntypedFormGroup { return this.getFormGroup(spec, [], current) } - getUnionObject( - spec: ValueSpecUnion | ListValueSpecUnion, - selection: string, - current?: { [key: string]: any } | null, - ): UntypedFormGroup { - const { variants, tag } = spec - const { name, description, warning, 'variant-names': variantNames } = tag - - const enumSpec: ValueSpecEnum = { - type: 'enum', - name, - description, - warning, + getUnionSelectSpec( + spec: IST.ValueSpecUnion, + selection: string | null, + ): IST.ValueSpecSelect { + return { + ...spec, + type: 'select', default: selection, - values: Object.keys(variants), - 'value-names': variantNames, + values: Object.fromEntries( + Object.entries(spec.variants).map(([key, { name }]) => [key, name]), + ), } - return this.getFormGroup( - { [spec.tag.id]: enumSpec, ...spec.variants[selection] }, - [], - current, - ) } - getListItem(spec: ValueSpecList, entry: any) { - const listItemValidators = getListItemValidators(spec) - if (isValueSpecListOf(spec, 'string')) { - return this.formBuilder.control(entry, listItemValidators) - } else if (isValueSpecListOf(spec, 'number')) { - return this.formBuilder.control(entry, listItemValidators) - } else if (isValueSpecListOf(spec, 'enum')) { - return this.formBuilder.control(entry) - } else if (isValueSpecListOf(spec, 'object')) { - return this.getFormGroup(spec.spec.spec, listItemValidators, entry) - } else if (isValueSpecListOf(spec, 'union')) { - return this.getUnionObject(spec.spec, spec.spec.default, entry) + getUnionObject(spec: IST.ValueSpecUnion, value: any): UntypedFormGroup { + const valid = spec.variants[value?.selection] + const selected = valid ? value?.selection : spec.default + const selection = this.getUnionSelectSpec(spec, selected) + const group = this.getFormGroup({ selection }) + const control = selected ? spec.variants[selected].spec : {} + + group.setControl('value', this.getFormGroup(control, [], value?.value)) + + return group + } + + getListItem(spec: IST.ValueSpecList, entry?: any) { + if (IST.isValueSpecListOf(spec, 'text')) { + return this.formBuilder.control(entry, [ + ...stringValidators(spec.spec), + Validators.required, + ]) + } else if (IST.isValueSpecListOf(spec, 'object')) { + return this.getFormGroup(spec.spec.spec, [], entry) } } - private getFormGroup( - config: ConfigSpec, + getFormGroup( + config: IST.InputSpec, validators: ValidatorFn[] = [], - current?: { [key: string]: any } | null, + current?: Record | null, ): UntypedFormGroup { let group: Record< string, UntypedFormGroup | UntypedFormArray | UntypedFormControl > = {} Object.entries(config).map(([key, spec]) => { - if (spec.type === 'pointer') return group[key] = this.getFormEntry(spec, current ? current[key] : undefined) }) return this.formBuilder.group(group, { validators }) } private getFormEntry( - spec: ValueSpec, + spec: IST.ValueSpec, currentValue?: any, ): UntypedFormGroup | UntypedFormArray | UntypedFormControl { - let validators: ValidatorFn[] let value: any switch (spec.type) { - case 'string': - validators = stringValidators(spec) + case 'text': if (currentValue !== undefined) { value = currentValue } else { - value = spec.default ? getDefaultString(spec.default) : null + value = spec.default ? utils.getDefaultString(spec.default) : null } - return this.formBuilder.control(value, validators) + return this.formBuilder.control(value, stringValidators(spec)) + case 'textarea': + value = currentValue || null + return this.formBuilder.control(value, textareaValidators(spec)) case 'number': - validators = numberValidators(spec) if (currentValue !== undefined) { value = currentValue } else { value = spec.default || null } - return this.formBuilder.control(value, validators) + return this.formBuilder.control(value, numberValidators(spec)) + case 'color': + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, colorValidators(spec)) + case 'datetime': + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, datetimeValidators(spec)) case 'object': return this.getFormGroup(spec.spec, [], currentValue) case 'list': - validators = listValidators(spec) - const mapped = ( - Array.isArray(currentValue) ? currentValue : (spec.default as any[]) - ).map(entry => { - return this.getListItem(spec, entry) - }) - return this.formBuilder.array(mapped, validators) - case 'union': - const currentSelection = currentValue?.[spec.tag.id] - const isValid = !!spec.variants[currentSelection] - - return this.getUnionObject( - spec, - isValid ? currentSelection : spec.default, - isValid ? currentValue : undefined, + const array = Array.isArray(currentValue) ? currentValue : spec.default + const length = Math.max(array.length, spec.minLength || 0) + + return this.formBuilder.array( + Array.from({ length }).map((_, index) => + this.getListItem(spec, array[index]), + ), + listValidators(spec), ) - case 'boolean': - case 'enum': + case 'union': + return this.getUnionObject(spec, currentValue) + case 'toggle': value = currentValue === undefined ? spec.default : currentValue return this.formBuilder.control(value) + case 'select': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value, [Validators.required]) + case 'multiselect': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value, multiselectValidators(spec)) default: return this.formBuilder.control(null) } } } -function getListItemValidators(spec: ValueSpecList) { - if (isValueSpecListOf(spec, 'string')) { - return stringValidators(spec.spec) - } else if (isValueSpecListOf(spec, 'number')) { - return numberValidators(spec.spec) - } -} +// function getListItemValidators(spec: IST.ValueSpecList) { +// if (IST.isValueSpecListOf(spec, 'text')) { +// return stringValidators(spec.spec) +// } +// } function stringValidators( - spec: ValueSpecString | ListValueSpecString, + spec: IST.ValueSpecText | IST.ListValueSpecText, ): ValidatorFn[] { const validators: ValidatorFn[] = [] - if (!(spec as ValueSpecString).nullable) { + if ((spec as IST.ValueSpecText).required) { validators.push(Validators.required) } - if (spec.pattern) { - validators.push(Validators.pattern(spec.pattern)) + validators.push(textLengthInRange(spec.minLength, spec.maxLength)) + + if (spec.patterns.length) { + spec.patterns.forEach(p => validators.push(Validators.pattern(p.regex))) } return validators } -function numberValidators( - spec: ValueSpecNumber | ListValueSpecNumber, -): ValidatorFn[] { +function textareaValidators(spec: IST.ValueSpecTextarea): ValidatorFn[] { const validators: ValidatorFn[] = [] - validators.push(isNumber()) + if (spec.required) { + validators.push(Validators.required) + } + + validators.push(textLengthInRange(spec.minLength, spec.maxLength)) + + return validators +} - if (!(spec as ValueSpecNumber).nullable) { +function colorValidators({ required }: IST.ValueSpecColor): ValidatorFn[] { + const validators: ValidatorFn[] = [Validators.pattern(/^#[0-9a-f]{6}$/i)] + + if (required) { validators.push(Validators.required) } - if (spec.integral) { - validators.push(isInteger()) + return validators +} + +function datetimeValidators({ + required, + min, + max, +}: IST.ValueSpecDatetime): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (required) { + validators.push(Validators.required) } - validators.push(numberInRange(spec.range)) + if (min) { + validators.push(datetimeMin(min)) + } + + if (max) { + validators.push(datetimeMax(max)) + } return validators } -function listValidators(spec: ValueSpecList): ValidatorFn[] { +function numberValidators(spec: IST.ValueSpecNumber): ValidatorFn[] { const validators: ValidatorFn[] = [] - validators.push(listInRange(spec.range)) + validators.push(isNumber()) - validators.push(listItemIssue()) + if ((spec as IST.ValueSpecNumber).required) { + validators.push(Validators.required) + } - if (!isValueSpecListOf(spec, 'enum')) { - validators.push(listUnique(spec)) + if (spec.integer) { + validators.push(isInteger()) } + validators.push(numberInRange(spec.min, spec.max)) + + return validators +} + +function multiselectValidators(spec: IST.ValueSpecMultiselect): ValidatorFn[] { + const validators: ValidatorFn[] = [] + validators.push(listInRange(spec.minLength, spec.maxLength)) + return validators +} + +function listValidators(spec: IST.ValueSpecList): ValidatorFn[] { + const validators: ValidatorFn[] = [] + validators.push(listInRange(spec.minLength, spec.maxLength)) + validators.push(listItemIssue()) return validators } -export function numberInRange(stringRange: string): ValidatorFn { +export function numberInRange( + min: number | null, + max: number | null, +): ValidatorFn { return control => { const value = control.value - if (!value) return null - try { - Range.from(stringRange).checkIncludes(value) - return null - } catch (e: any) { - return { numberNotInRange: { value: `Number must be ${e.message}` } } - } + if (typeof value !== 'number') return null + if (min && value < min) + return { + numberNotInRange: `Number must be greater than or equal to ${min}`, + } + if (max && value > max) + return { numberNotInRange: `Number must be less than or equal to ${max}` } + return null } } export function isNumber(): ValidatorFn { - return control => - !control.value || control.value == Number(control.value) - ? null - : { notNumber: { value: control.value } } + return ({ value }) => + !value || value == Number(value) ? null : { notNumber: 'Must be a number' } } export function isInteger(): ValidatorFn { - return control => - !control.value || control.value == Math.trunc(control.value) + return ({ value }) => + !value || value == Math.trunc(value) ? null - : { numberNotInteger: { value: control.value } } + : { numberNotInteger: 'Must be an integer' } } -export function listInRange(stringRange: string): ValidatorFn { +export function listInRange( + minLength: number | null, + maxLength: number | null, +): ValidatorFn { return control => { - try { - Range.from(stringRange).checkIncludes(control.value.length) - return null - } catch (e: any) { - return { listNotInRange: { value: `List must be ${e.message}` } } - } + const length = control.value.length + if (minLength && length < minLength) + return { + listNotInRange: `List must contain at least ${minLength} entries`, + } + if (maxLength && length > maxLength) + return { + listNotInRange: `List cannot contain more than ${maxLength} entries`, + } + return null + } +} + +export function datetimeMin(min: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const minDate = new Date(min.length === 5 ? `2000-01-01T${min}` : min) + + return date < minDate ? { datetimeMin: `Minimum is ${min}` } : null + } +} + +export function datetimeMax(max: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const maxDate = new Date(max.length === 5 ? `2000-01-01T${max}` : max) + + return date > maxDate ? { datetimeMin: `Maximum is ${max}` } : null + } +} + +export function textLengthInRange( + minLength: number | null, + maxLength: number | null, +): ValidatorFn { + return control => { + const value = control.value + if (value === null || value === undefined) return null + + const length = value.length + if (minLength && length < minLength) + return { listNotInRange: `Must be at least ${minLength} characters` } + if (maxLength && length > maxLength) + return { listNotInRange: `Cannot be great than ${maxLength} characters` } + return null } } @@ -248,36 +331,33 @@ export function listItemIssue(): ValidatorFn { const { controls } = parentControl as UntypedFormArray const problemChild = controls.find(c => c.invalid) if (problemChild) { - return { listItemIssue: { value: 'Invalid entries' } } + return { listItemIssue: 'Invalid entries' } } else { return null } } } -export function listUnique(spec: ValueSpecList): ValidatorFn { +export function listUnique(spec: IST.ValueSpecList): ValidatorFn { return control => { const list = control.value for (let idx = 0; idx < list.length; idx++) { for (let idx2 = idx + 1; idx2 < list.length; idx2++) { if (listItemEquals(spec, list[idx], list[idx2])) { + const objSpec = spec.spec let display1: string let display2: string - let uniqueMessage = isObjectOrUnion(spec.spec) - ? uniqueByMessageWrapper( - spec.spec['unique-by'], - spec.spec, - list[idx], - ) + let uniqueMessage = isObject(objSpec) + ? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec) : '' - if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) { + if (isObject(objSpec) && objSpec.displayAs) { display1 = `"${(Mustache as any).render( - spec.spec['display-as'], + objSpec.displayAs, list[idx], )}"` display2 = `"${(Mustache as any).render( - spec.spec['display-as'], + objSpec.displayAs, list[idx2], )}"` } else { @@ -286,9 +366,7 @@ export function listUnique(spec: ValueSpecList): ValidatorFn { } return { - listNotUnique: { - value: `${display1} and ${display2} are not unique.${uniqueMessage}`, - }, + listNotUnique: `${display1} and ${display2} are not unique.${uniqueMessage}`, } } } @@ -297,46 +375,44 @@ export function listUnique(spec: ValueSpecList): ValidatorFn { } } -function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean { +function listItemEquals( + spec: IST.ValueSpecList, + val1: any, + val2: any, +): boolean { // TODO: fix types - switch (spec.subtype) { - case 'string': - case 'number': - case 'enum': + switch (spec.spec.type) { + case 'text': return val1 == val2 case 'object': - const obj: ListValueSpecObject = spec.spec as any - - return listObjEquals(obj['unique-by'], obj, val1, val2) - case 'union': - const union: ListValueSpecUnion = spec.spec as any - - return unionEquals(union['unique-by'], union, val1, val2) + const obj = spec.spec + return listObjEquals(obj.uniqueBy, obj, val1, val2) default: return false } } -function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { +function itemEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean { switch (spec.type) { - case 'string': + case 'text': + case 'textarea': case 'number': - case 'boolean': - case 'enum': + case 'toggle': + case 'select': return val1 == val2 case 'object': // TODO: 'unique-by' does not exist on ValueSpecObject, fix types return objEquals( (spec as any)['unique-by'], - spec as ValueSpecObject, + spec as IST.ValueSpecObject, val1, val2, ) case 'union': - // TODO: 'unique-by' does not exist on ValueSpecUnion, fix types + // TODO: 'unique-by' does not exist onIST.ValueSpecUnion, fix types return unionEquals( (spec as any)['unique-by'], - spec as ValueSpecUnion, + spec as IST.ValueSpecUnion, val1, val2, ) @@ -356,12 +432,12 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { } function listObjEquals( - uniqueBy: UniqueBy, - spec: ListValueSpecObject, + uniqueBy: IST.UniqueBy, + spec: IST.ListValueSpecObject, val1: any, val2: any, ): boolean { - if (uniqueBy === null) { + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) @@ -384,12 +460,12 @@ function listObjEquals( } function objEquals( - uniqueBy: UniqueBy, - spec: ValueSpecObject, + uniqueBy: IST.UniqueBy, + spec: IST.ValueSpecObject, val1: any, val2: any, ): boolean { - if (uniqueBy === null) { + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { // TODO: fix types @@ -413,20 +489,19 @@ function objEquals( } function unionEquals( - uniqueBy: UniqueBy, - spec: ValueSpecUnion | ListValueSpecUnion, + uniqueBy: IST.UniqueBy, + spec: IST.ValueSpecUnion, val1: any, val2: any, ): boolean { - const tagId = spec.tag.id - const variant = spec.variants[val1[tagId]] - if (uniqueBy === null) { + const variantSpec = spec.variants[val1.selection].spec + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { - if (uniqueBy === tagId) { - return val1[tagId] === val2[tagId] + if (uniqueBy === 'selection') { + return val1.selection === val2.selection } else { - return itemEquals(variant[uniqueBy], val1[uniqueBy], val2[uniqueBy]) + return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) } } else if ('any' in uniqueBy) { for (let subSpec of uniqueBy.any) { @@ -447,20 +522,10 @@ function unionEquals( } function uniqueByMessageWrapper( - uniqueBy: UniqueBy, - spec: ListValueSpecObject | ListValueSpecUnion, - obj: Record, + uniqueBy: IST.UniqueBy, + spec: IST.ListValueSpecObject, ) { - let configSpec: ConfigSpec - if (isUnion(spec)) { - const tagId = spec.tag.id - configSpec = { - [tagId]: { name: spec.tag.name } as ValueSpec, - ...spec.variants[obj[tagId]], - } - } else { - configSpec = spec.spec - } + let configSpec = spec.spec const message = uniqueByMessage(uniqueBy, configSpec) if (message) { @@ -469,17 +534,17 @@ function uniqueByMessageWrapper( } function uniqueByMessage( - uniqueBy: UniqueBy, - configSpec: ConfigSpec, + uniqueBy: IST.UniqueBy, + configSpec: IST.InputSpec, outermost = true, ): string { let joinFunc const subSpecs: string[] = [] - if (uniqueBy === null) { + if (!uniqueBy) { return '' } else if (typeof uniqueBy === 'string') { return configSpec[uniqueBy] - ? (configSpec[uniqueBy] as ValueSpecObject).name + ? (configSpec[uniqueBy] as IST.ValueSpecObject).name : uniqueBy } else if ('any' in uniqueBy) { joinFunc = ' OR ' @@ -498,20 +563,15 @@ function uniqueByMessage( : '(' + ret + ')' } -function isObjectOrUnion( - spec: ListValueSpecOf, -): spec is ListValueSpecObject | ListValueSpecUnion { - // only lists of objects and unions have unique-by - return 'unique-by' in spec -} - -function isUnion(spec: any): spec is ListValueSpecUnion { - // only unions have tag - return !!spec.tag +function isObject( + spec: IST.ListValueSpecOf, +): spec is IST.ListValueSpecObject { + // only lists of objects have uniqueBy + return 'uniqueBy' in spec } export function convertValuesRecursive( - configSpec: ConfigSpec, + configSpec: IST.InputSpec, group: UntypedFormGroup, ) { Object.entries(configSpec).forEach(([key, valueSpec]) => { @@ -523,40 +583,27 @@ export function convertValuesRecursive( control.setValue( control.value || control.value === 0 ? Number(control.value) : null, ) - } else if (valueSpec.type === 'string') { + } else if (valueSpec.type === 'text' || valueSpec.type === 'textarea') { if (!control.value) control.setValue(null) } else if (valueSpec.type === 'object') { convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup) } else if (valueSpec.type === 'union') { const formGr = group.get(key) as UntypedFormGroup - const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value] + const spec = valueSpec.variants[formGr.controls['selection'].value].spec convertValuesRecursive(spec, formGr) } else if (valueSpec.type === 'list') { const formArr = group.get(key) as UntypedFormArray const { controls } = formArr - if (valueSpec.subtype === 'number') { - controls.forEach(control => { - control.setValue(control.value ? Number(control.value) : null) - }) - } else if (valueSpec.subtype === 'string') { + if (valueSpec.spec.type === 'text') { controls.forEach(control => { if (!control.value) control.setValue(null) }) - } else if (valueSpec.subtype === 'object') { + } else if (valueSpec.spec.type === 'object') { controls.forEach(formGroup => { - const objectSpec = valueSpec.spec as ListValueSpecObject + const objectSpec = valueSpec.spec as IST.ListValueSpecObject convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup) }) - } else if (valueSpec.subtype === 'union') { - controls.forEach(formGroup => { - const unionSpec = valueSpec.spec as ListValueSpecUnion - const spec = - unionSpec.variants[ - (formGroup as UntypedFormGroup).controls[unionSpec.tag.id].value - ] - convertValuesRecursive(spec, formGroup as UntypedFormGroup) - }) } } }) diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 9592e7da1..a080f051d 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { - MarketplacePkg, AbstractMarketplaceService, StoreData, Marketplace, - StoreInfo, StoreIdentity, + MarketplacePkg, + GetPackageRes, } from '@start9labs/marketplace' import { BehaviorSubject, @@ -33,13 +33,14 @@ import { tap, } from 'rxjs/operators' import { ConfigService } from './config.service' -import { sameUrl } from '@start9labs/shared' +import { Exver, sameUrl } from '@start9labs/shared' import { ClientStorageService } from './client-storage.service' +import { T } from '@start9labs/start-sdk' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { private readonly knownHosts$: Observable = this.patch - .watch$('ui', 'marketplace', 'known-hosts') + .watch$('ui', 'marketplace', 'knownHosts') .pipe( map(hosts => { const { start9, community } = this.config.marketplace @@ -73,40 +74,42 @@ export class MarketplaceService implements AbstractMarketplaceService { private readonly selectedHost$: Observable = this.patch .watch$('ui', 'marketplace') .pipe( - distinctUntilKeyChanged('selected-url'), - map(({ 'selected-url': url, 'known-hosts': hosts }) => + distinctUntilKeyChanged('selectedUrl'), + map(({ selectedUrl: url, knownHosts: hosts }) => toStoreIdentity(url, hosts[url]), ), shareReplay({ bufferSize: 1, refCount: true }), ) - private readonly marketplace$ = this.knownHosts$.pipe( - startWith([]), - pairwise(), - mergeMap(([prev, curr]) => - curr.filter(c => !prev.find(p => sameUrl(c.url, p.url))), - ), - mergeMap(({ url, name }) => - this.fetchStore$(url).pipe( - tap(data => { - if (data?.info) this.updateStoreName(url, name, data.info.name) - }), - map(data => { - return [url, data] - }), - startWith<[string, StoreData | null]>([url, null]), + private readonly marketplace$: Observable = + this.knownHosts$.pipe( + startWith([]), + pairwise(), + mergeMap(([prev, curr]) => + curr.filter(c => !prev.find(p => sameUrl(c.url, p.url))), ), - ), - scan<[string, StoreData | null], Record>( - (requests, [url, store]) => { - requests[url] = store + mergeMap(({ url, name }) => + this.fetchStore$(url).pipe( + tap(data => { + if (data?.info.name) this.updateStoreName(url, name, data.info.name) + }), + map(data => [ + url, + data, + ]), + startWith<[string, StoreData | null]>([url, null]), + ), + ), + scan<[string, StoreData | null], Record>( + (requests, [url, store]) => { + requests[url] = store - return requests - }, - {}, - ), - shareReplay({ bufferSize: 1, refCount: true }), - ) + return requests + }, + {}, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ) private readonly filteredMarketplace$ = combineLatest([ this.clientStorageService.showDevTools$, @@ -144,6 +147,7 @@ export class MarketplaceService implements AbstractMarketplaceService { private readonly patch: PatchDB, private readonly config: ConfigService, private readonly clientStorageService: ClientStorageService, + private readonly exver: Exver, ) {} getKnownHosts$(filtered = false): Observable { @@ -166,28 +170,29 @@ export class MarketplaceService implements AbstractMarketplaceService { getPackage$( id: string, - version: string, - optionalUrl?: string, + version: string | null, + flavor: string | null, + registryUrl?: string, ): Observable { - return this.patch.watch$('ui', 'marketplace').pipe( - switchMap(uiMarketplace => { - const url = optionalUrl || uiMarketplace['selected-url'] + return this.selectedHost$.pipe( + switchMap(selected => + this.marketplace$.pipe( + switchMap(m => { + const url = registryUrl || selected.url - if (version !== '*' || !uiMarketplace['known-hosts'][url]) { - return this.fetchPackage$(id, version, url) - } + const pkg = m[url]?.packages.find( + p => + p.id === id && + p.flavor === flavor && + (!version || this.exver.compareExver(p.version, version) === 0), + ) - return this.marketplace$.pipe( - map(m => m[url]), - filter(Boolean), - take(1), - map( - store => - store.packages.find(p => p.manifest.id === id) || - ({} as MarketplacePkg), - ), - ) - }), + return !!pkg + ? of(pkg) + : this.fetchPackage$(url, id, version, flavor) + }), + ), + ), ) } @@ -206,56 +211,22 @@ export class MarketplaceService implements AbstractMarketplaceService { ): Promise { const params: RR.InstallPackageReq = { id, - 'version-spec': `=${version}`, - 'marketplace-url': url, + version, + registry: url, } await this.api.installPackage(params) } - fetchInfo$(url: string): Observable { - return this.patch.watch$('server-info').pipe( - take(1), - switchMap(serverInfo => { - const qp: RR.GetMarketplaceInfoReq = { 'server-id': serverInfo.id } - return this.api.marketplaceProxy( - '/package/v0/info', - qp, - url, - ) - }), - ) + fetchInfo$(url: string): Observable { + return from(this.api.getRegistryInfo(url)) } - fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy>( - `/package/v0/release-notes/${id}`, - {}, - url || m.url, - ), - ) - }), - ) - } - - fetchStatic$(id: string, type: string, url?: string): Observable { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy( - `/package/v0/${type}/${id}`, - {}, - url || m.url, - ), - ) - }), - ) + fetchStatic$( + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', + ): Observable { + return from(this.api.getStaticProxy(pkg, type)) } private fetchStore$(url: string): Observable { @@ -269,33 +240,62 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - private fetchPackages$( - url: string, - params: Omit = {}, - ): Observable { - const qp: RR.GetMarketplacePackagesReq = { - ...params, - page: 1, - 'per-page': 100, - } - if (qp.ids) qp.ids = JSON.stringify(qp.ids) - - return from( - this.api.marketplaceProxy( - '/package/v0/index', - qp, - url, - ), + private fetchPackages$(url: string): Observable { + return from(this.api.getRegistryPackages(url)).pipe( + map(packages => { + return Object.entries(packages).flatMap(([id, pkgInfo]) => + Object.keys(pkgInfo.best).map(version => + this.convertToMarketplacePkg( + id, + version, + this.exver.getFlavor(version), + pkgInfo, + ), + ), + ) + }), ) } - private fetchPackage$( + convertToMarketplacePkg( id: string, - version: string, + version: string | null, + flavor: string | null, + pkgInfo: GetPackageRes, + ): MarketplacePkg { + version = + version || + Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) || + null + + return !version || !pkgInfo.best[version] + ? ({} as MarketplacePkg) + : { + id, + version, + flavor, + ...pkgInfo, + ...pkgInfo.best[version], + } + } + + private fetchPackage$( url: string, + id: string, + version: string | null, + flavor: string | null, ): Observable { - return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( - map(pkgs => pkgs[0] || {}), + return from( + this.api.getRegistryPackage(url, id, version ? `=${version}` : null), + ).pipe( + map(pkgInfo => + this.convertToMarketplacePkg( + id, + version === '*' ? null : version, + flavor, + pkgInfo, + ), + ), ) } @@ -306,7 +306,7 @@ export class MarketplaceService implements AbstractMarketplaceService { ): Promise { if (oldName !== newName) { this.api.setDbValue( - ['marketplace', 'known-hosts', url, 'name'], + ['marketplace', 'knownHosts', url, 'name'], newName, ) } diff --git a/web/projects/ui/src/app/services/modal.service.ts b/web/projects/ui/src/app/services/modal.service.ts deleted file mode 100644 index c34fce9a2..000000000 --- a/web/projects/ui/src/app/services/modal.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { DependentInfo } from 'src/app/types/dependent-info' -import { AppConfigPage } from 'src/app/modals/app-config/app-config.page' - -@Injectable({ - providedIn: 'root', -}) -export class ModalService { - constructor(private readonly modalCtrl: ModalController) {} - - async presentModalConfig(componentProps: ComponentProps): Promise { - const modal = await this.modalCtrl.create({ - component: AppConfigPage, - componentProps, - }) - await modal.present() - } -} - -interface ComponentProps { - pkgId: string - dependentInfo?: DependentInfo -} diff --git a/web/projects/ui/src/app/services/network.service.ts b/web/projects/ui/src/app/services/network.service.ts new file mode 100644 index 000000000..e1568603d --- /dev/null +++ b/web/projects/ui/src/app/services/network.service.ts @@ -0,0 +1,22 @@ +import { inject, Injectable } from '@angular/core' +import { WINDOW } from '@ng-web-apis/common' +import { fromEvent, merge, Observable, shareReplay } from 'rxjs' +import { distinctUntilChanged, map, startWith } from 'rxjs/operators' + +@Injectable({ providedIn: 'root' }) +export class NetworkService extends Observable { + private readonly win = inject(WINDOW) + private readonly stream$ = merge( + fromEvent(this.win, 'online'), + fromEvent(this.win, 'offline'), + ).pipe( + startWith(null), + map(() => this.win.navigator.onLine), + distinctUntilChanged(), + shareReplay(1), + ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/services/patch-data.service.ts b/web/projects/ui/src/app/services/patch-data.service.ts index 23ee15925..35728c9ec 100644 --- a/web/projects/ui/src/app/services/patch-data.service.ts +++ b/web/projects/ui/src/app/services/patch-data.service.ts @@ -1,31 +1,29 @@ import { Inject, Injectable } from '@angular/core' -import { ModalController } from '@ionic/angular' import { Observable } from 'rxjs' -import { filter, share, switchMap, take, tap } from 'rxjs/operators' +import { filter, map, share, switchMap, take } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' -import { DataModel, UIData } from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { EOSService } from 'src/app/services/eos.service' -import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page' -import { ConfigService } from 'src/app/services/config.service' -import { ApiService } from 'src/app/services/api/embassy-api.service' import { MarketplaceService } from 'src/app/services/marketplace.service' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ConnectionService } from 'src/app/services/connection.service' +import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' // Get data from PatchDb after is starts and act upon it @Injectable({ providedIn: 'root', }) -export class PatchDataService extends Observable { - private readonly stream$ = this.connectionService.connected$.pipe( +export class PatchDataService extends Observable { + private readonly stream$ = this.connection$.pipe( filter(Boolean), switchMap(() => this.patch.watch$()), - take(1), - tap(({ ui }) => { - // check for updates to eOS and services - this.checkForUpdates() - // show eos welcome message - this.showEosWelcome(ui['ack-welcome']) + map((cache, index) => { + this.bootstrapper.update(cache) + + if (index === 0) { + // check for updates to StartOS and services + this.checkForUpdates() + } }), share(), ) @@ -33,12 +31,10 @@ export class PatchDataService extends Observable { constructor( private readonly patch: PatchDB, private readonly eosService: EOSService, - private readonly config: ConfigService, - private readonly modalCtrl: ModalController, - private readonly embassyApi: ApiService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly connectionService: ConnectionService, + private readonly connection$: ConnectionService, + private readonly bootstrapper: LocalStorageBootstrap, ) { super(subscriber => this.stream$.subscribe(subscriber)) } @@ -47,23 +43,4 @@ export class PatchDataService extends Observable { this.eosService.loadEos() this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe() } - - private async showEosWelcome(ackVersion: string): Promise { - if (this.config.skipStartupAlerts || ackVersion === this.config.version) { - return - } - - const modal = await this.modalCtrl.create({ - component: OSWelcomePage, - presentingElement: await this.modalCtrl.getTop(), - backdropDismiss: false, - }) - modal.onWillDismiss().then(() => { - this.embassyApi - .setDbValue(['ack-welcome'], this.config.version) - .catch() - }) - - await modal.present() - } } diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index e4ea729d9..01f80c3a7 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -1,25 +1,19 @@ -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { Url } from '@start9labs/shared' -import { MarketplaceManifest } from '@start9labs/marketplace' -import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' +import { T } from '@start9labs/start-sdk' -export interface DataModel { - 'server-info': ServerInfo - 'package-data': { [id: string]: PackageDataEntry } +export type DataModel = T.Public & { ui: UIData + packageData: Record } export interface UIData { name: string | null - 'ack-welcome': string // eOS emver marketplace: UIMarketplaceData - dev: DevData gaming: { snake: { - 'high-score': number + highScore: number } } - 'ack-instructions': Record + ackInstructions: Record theme: string widgets: readonly Widget[] } @@ -38,8 +32,8 @@ export interface Widget { } export interface UIMarketplaceData { - 'selected-url': string - 'known-hosts': { + selectedUrl: string + knownHosts: { 'https://registry.start9.com/': UIStore 'https://community-registry.start9.com/': UIStore [url: string]: UIStore @@ -50,327 +44,36 @@ export interface UIStore { name?: string } -export interface DevData { - [id: string]: DevProjectData -} - -export interface DevProjectData { - name: string - instructions: string - config: string - 'basic-info'?: BasicInfo -} - -export interface ServerInfo { - id: string - version: string - 'last-backup': string | null - 'lan-address': Url - 'tor-address': Url - 'ip-info': IpInfo - 'last-wifi-region': string | null - 'unread-notification-count': number - 'status-info': ServerStatusInfo - 'eos-version-compat': string - 'password-hash': string - hostname: string - pubkey: string - 'ca-fingerprint': string - 'ntp-synced': boolean - zram: boolean - platform: string -} - -export interface IpInfo { - [iface: string]: { - ipv4: string | null - ipv6: string | null - } -} - -export interface ServerStatusInfo { - 'backup-progress': null | { - [packageId: string]: { - complete: boolean - } - } - updated: boolean - 'update-progress': { size: number | null; downloaded: number } | null - restarting: boolean - 'shutting-down': boolean -} - -export enum ServerStatus { - Running = 'running', - Updated = 'updated', - BackingUp = 'backing-up', -} - -export interface PackageDataEntry { - state: PackageState - 'static-files': { - license: Url - instructions: Url - icon: Url - } - manifest: Manifest - installed?: InstalledPackageDataEntry // exists when: installed, updating - 'install-progress'?: InstallProgress // exists when: installing, updating -} - -export enum PackageState { - Installing = 'installing', - Installed = 'installed', - Updating = 'updating', - Removing = 'removing', - Restoring = 'restoring', -} - -export interface InstalledPackageDataEntry { - status: Status - manifest: Manifest - 'last-backup': string | null - 'system-pointers': any[] - 'current-dependents': { [id: string]: CurrentDependencyInfo } - 'current-dependencies': { [id: string]: CurrentDependencyInfo } - 'dependency-info': { - [id: string]: { - title: string - icon: Url - } - } - 'interface-addresses': { - [id: string]: { 'tor-address': string; 'lan-address': string } +export type PackageDataEntry = + T.PackageDataEntry & { + stateInfo: T } - 'marketplace-url': string | null - 'developer-key': string -} - -export interface CurrentDependencyInfo { - pointers: any[] - 'health-checks': string[] // array of health check IDs -} - -export interface Manifest extends MarketplaceManifest { - assets: { - license: string // filename - instructions: string // filename - icon: string // filename - docker_images: string // filename - assets: string // path to assets folder - scripts: string // path to scripts folder - } - main: ActionImpl - 'health-checks': Record< - string, - ActionImpl & { name: string; 'success-message': string | null } - > - config: ConfigActions | null - volumes: Record - 'min-os-version': string - interfaces: Record - backup: BackupActions - migrations: Migrations | null - actions: Record -} - -export interface DependencyConfig { - check: ActionImpl - 'auto-configure': ActionImpl -} - -export interface ActionImpl { - type: 'docker' - image: string - system: boolean - entrypoint: string - args: string[] - mounts: { [id: string]: string } - 'io-format': DockerIoFormat | null - inject: boolean - 'shm-size': string - 'sigterm-timeout': string | null -} - -export enum DockerIoFormat { - Json = 'json', - Yaml = 'yaml', - Cbor = 'cbor', - Toml = 'toml', -} - -export interface ConfigActions { - get: ActionImpl | null - set: ActionImpl | null -} - -export type Volume = VolumeData - -export interface VolumeData { - type: VolumeType.Data - readonly: boolean -} - -export interface VolumeAssets { - type: VolumeType.Assets -} -export interface VolumePointer { - type: VolumeType.Pointer - 'package-id': string - 'volume-id': string - path: string - readonly: boolean -} - -export interface VolumeCertificate { - type: VolumeType.Certificate - 'interface-id': string -} - -export interface VolumeBackup { - type: VolumeType.Backup - readonly: boolean -} - -export enum VolumeType { - Data = 'data', - Assets = 'assets', - Pointer = 'pointer', - Certificate = 'certificate', - Backup = 'backup', -} - -export interface InterfaceDef { - name: string - description: string - 'tor-config': TorConfig | null - 'lan-config': LanConfig | null - ui: boolean - protocols: string[] -} - -export interface TorConfig { - 'port-mapping': { [port: number]: number } -} - -export type LanConfig = { - [port: number]: { ssl: boolean; mapping: number } -} - -export interface BackupActions { - create: ActionImpl - restore: ActionImpl -} - -export interface Migrations { - from: { [versionRange: string]: ActionImpl } - to: { [versionRange: string]: ActionImpl } -} - -export interface Action { - name: string - description: string - warning: string | null - implementation: ActionImpl - 'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[] - 'input-spec': ConfigSpec | null -} - -export interface Status { - configured: boolean - main: MainStatus - 'dependency-config-errors': { [id: string]: string | null } -} +export type AllPackageData = NonNullable< + T.AllPackageData & Record> +> -export type MainStatus = - | MainStatusStopped - | MainStatusStopping - | MainStatusStarting - | MainStatusRunning - | MainStatusBackingUp - | MainStatusRestarting - -export interface MainStatusStopped { - status: PackageMainStatus.Stopped -} - -export interface MainStatusStopping { - status: PackageMainStatus.Stopping -} - -export interface MainStatusStarting { - status: PackageMainStatus.Starting - restarting: boolean -} - -export interface MainStatusRunning { - status: PackageMainStatus.Running - started: string // UTC date string - health: { [id: string]: HealthCheckResult } -} - -export interface MainStatusBackingUp { - status: PackageMainStatus.BackingUp - started: string | null // UTC date string -} - -export interface MainStatusRestarting { - status: PackageMainStatus.Restarting -} - -export enum PackageMainStatus { - Starting = 'starting', - Running = 'running', - Stopping = 'stopping', - Stopped = 'stopped', - BackingUp = 'backing-up', - Restarting = 'restarting', -} - -export type HealthCheckResult = - | HealthCheckResultStarting - | HealthCheckResultLoading - | HealthCheckResultDisabled - | HealthCheckResultSuccess - | HealthCheckResultFailure - -export enum HealthResult { - Starting = 'starting', - Loading = 'loading', - Disabled = 'disabled', - Success = 'success', - Failure = 'failure', -} - -export interface HealthCheckResultStarting { - result: HealthResult.Starting -} - -export interface HealthCheckResultDisabled { - result: HealthResult.Disabled -} +export type StateInfo = InstalledState | InstallingState | UpdatingState -export interface HealthCheckResultSuccess { - result: HealthResult.Success +export type InstalledState = { + state: 'installed' | 'removing' + manifest: T.Manifest + installingInfo?: undefined } -export interface HealthCheckResultLoading { - result: HealthResult.Loading - message: string +export type InstallingState = { + state: 'installing' | 'restoring' + installingInfo: InstallingInfo + manifest?: undefined } -export interface HealthCheckResultFailure { - result: HealthResult.Failure - error: string +export type UpdatingState = { + state: 'updating' + installingInfo: InstallingInfo + manifest: T.Manifest } -export interface InstallProgress { - readonly size: number | null - readonly downloaded: number - readonly 'download-complete': boolean - readonly validated: number - readonly 'validation-complete': boolean - readonly unpacked: number - readonly 'unpack-complete': boolean +export type InstallingInfo = { + progress: T.FullProgress + newManifest: T.Manifest } diff --git a/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts b/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts index 2ea5bef02..079def855 100644 --- a/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts +++ b/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts @@ -1,4 +1,4 @@ -import { Bootstrapper, DBCache } from 'patch-db-client' +import { Dump } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { Injectable } from '@angular/core' import { StorageService } from '../storage.service' @@ -6,20 +6,18 @@ import { StorageService } from '../storage.service' @Injectable({ providedIn: 'root', }) -export class LocalStorageBootstrap implements Bootstrapper { - static CONTENT_KEY = 'patch-db-cache' +export class LocalStorageBootstrap { + static CONTENT_KEY = 'patchDB' constructor(private readonly storage: StorageService) {} - init(): DBCache { - const cache = this.storage.get>( - LocalStorageBootstrap.CONTENT_KEY, - ) + init(): Dump { + const cache = this.storage.get(LocalStorageBootstrap.CONTENT_KEY) - return cache || { sequence: 0, data: {} as DataModel } + return cache ? { id: 1, value: cache } : { id: 0, value: {} as DataModel } } - update(cache: DBCache): void { + update(cache: DataModel): void { this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache) } } diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts new file mode 100644 index 000000000..5d1f1ed51 --- /dev/null +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -0,0 +1,58 @@ +import { inject, Injectable, InjectionToken } from '@angular/core' +import { Dump, Revision, Update } from 'patch-db-client' +import { BehaviorSubject, EMPTY, Observable } from 'rxjs' +import { + bufferTime, + catchError, + filter, + skip, + startWith, + switchMap, + take, +} from 'rxjs/operators' +import { StateService } from 'src/app/services/state.service' +import { ApiService } from '../api/embassy-api.service' +import { AuthService } from '../auth.service' +import { DataModel } from './data-model' +import { LocalStorageBootstrap } from './local-storage-bootstrap' + +export const PATCH_CACHE = new InjectionToken('', { + factory: () => + new BehaviorSubject>({ + id: 0, + value: {} as DataModel, + }), +}) + +@Injectable({ + providedIn: 'root', +}) +export class PatchDbSource extends Observable[]> { + private readonly api = inject(ApiService) + private readonly state = inject(StateService) + private readonly stream$ = inject(AuthService).isVerified$.pipe( + switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)), + switchMap(({ dump, guid }) => + this.api.openWebsocket$(guid).pipe( + bufferTime(250), + filter(revisions => !!revisions.length), + startWith([dump]), + ), + ), + catchError((_, original$) => { + this.state.retrigger() + + return this.state.pipe( + skip(1), // skipping previous value stored due to shareReplay + filter(current => current === 'running'), + take(1), + switchMap(() => original$), + ) + }), + startWith([inject(LocalStorageBootstrap).init()]), + ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts b/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts deleted file mode 100644 index 51d29edc8..000000000 --- a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { InjectionToken, Injector } from '@angular/core' -import { - bufferTime, - catchError, - filter, - switchMap, - take, - tap, -} from 'rxjs/operators' -import { Update } from 'patch-db-client' -import { DataModel } from './data-model' -import { defer, EMPTY, from, interval, Observable } from 'rxjs' -import { AuthService } from '../auth.service' -import { ConnectionService } from '../connection.service' -import { ApiService } from '../api/embassy-api.service' -import { ConfigService } from '../config.service' - -export const PATCH_SOURCE = new InjectionToken[]>>( - '', -) - -export function sourceFactory( - injector: Injector, -): Observable[]> { - // defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there - return defer(() => { - const api = injector.get(ApiService) - const authService = injector.get(AuthService) - const connectionService = injector.get(ConnectionService) - const configService = injector.get(ConfigService) - const isTor = configService.isTor() - const timeout = isTor ? 16000 : 4000 - - const websocket$ = api.openPatchWebsocket$().pipe( - bufferTime(250), - filter(updates => !!updates.length), - catchError((_, watch$) => { - connectionService.websocketConnected$.next(false) - - return interval(timeout).pipe( - switchMap(() => - from(api.echo({ message: 'ping', timeout })).pipe( - catchError(() => EMPTY), - ), - ), - take(1), - switchMap(() => watch$), - ) - }), - tap(() => connectionService.websocketConnected$.next(true)), - ) - - return authService.isVerified$.pipe( - switchMap(verified => (verified ? websocket$ : EMPTY)), - ) - }) -} diff --git a/web/projects/ui/src/app/services/patch-db/patch-db.module.ts b/web/projects/ui/src/app/services/patch-db/patch-db.module.ts deleted file mode 100644 index 3c816e339..000000000 --- a/web/projects/ui/src/app/services/patch-db/patch-db.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PatchDB } from 'patch-db-client' -import { Injector, NgModule } from '@angular/core' -import { PATCH_SOURCE, sourceFactory } from './patch-db.factory' - -// This module is purely for providers organization purposes -@NgModule({ - providers: [ - { - provide: PATCH_SOURCE, - deps: [Injector], - useFactory: sourceFactory, - }, - { - provide: PatchDB, - deps: [PATCH_SOURCE], - useClass: PatchDB, - }, - ], -}) -export class PatchDbModule {} diff --git a/web/projects/ui/src/app/services/patch-monitor.service.ts b/web/projects/ui/src/app/services/patch-monitor.service.ts index cafb9f0fe..675531dda 100644 --- a/web/projects/ui/src/app/services/patch-monitor.service.ts +++ b/web/projects/ui/src/app/services/patch-monitor.service.ts @@ -4,24 +4,19 @@ import { tap } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { AuthService } from 'src/app/services/auth.service' import { DataModel } from './patch-db/data-model' -import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' // Start and stop PatchDb upon verification @Injectable({ providedIn: 'root', }) -export class PatchMonitorService extends Observable { - // @TODO not happy with Observable +export class PatchMonitorService extends Observable { private readonly stream$ = this.authService.isVerified$.pipe( - tap(verified => - verified ? this.patch.start(this.bootstrapper) : this.patch.stop(), - ), + tap(verified => (verified ? this.patch.start() : this.patch.stop())), ) constructor( private readonly authService: AuthService, private readonly patch: PatchDB, - private readonly bootstrapper: LocalStorageBootstrap, ) { super(subscriber => this.stream$.subscribe(subscriber)) } diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index 28c58a809..44543b15c 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,17 +1,11 @@ -import { isEmptyObject } from '@start9labs/shared' -import { - MainStatusStarting, - PackageDataEntry, - PackageMainStatus, - PackageState, - Status, -} from 'src/app/services/patch-db/data-model' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PkgDependencyErrors } from './dep-error.service' +import { T } from '@start9labs/start-sdk' export interface PackageStatus { primary: PrimaryStatus dependency: DependencyStatus | null - health: HealthStatus | null + health: T.HealthStatus | null } export function renderPkgStatus( @@ -20,65 +14,55 @@ export function renderPkgStatus( ): PackageStatus { let primary: PrimaryStatus let dependency: DependencyStatus | null = null - let health: HealthStatus | null = null + let health: T.HealthStatus | null = null - if (pkg.state === PackageState.Installed && pkg.installed) { - primary = getPrimaryStatus(pkg.installed.status) + if (pkg.stateInfo.state === 'installed') { + primary = getInstalledPrimaryStatus(pkg) dependency = getDependencyStatus(depErrors) - health = getHealthStatus( - pkg.installed.status, - !isEmptyObject(pkg.manifest['health-checks']), - ) + health = getHealthStatus(pkg.status) } else { - primary = pkg.state as string as PrimaryStatus + primary = pkg.stateInfo.state } return { primary, dependency, health } } -function getPrimaryStatus(status: Status): PrimaryStatus { - if (!status.configured) { - return PrimaryStatus.NeedsConfig - } else if ((status.main as MainStatusStarting).restarting) { - return PrimaryStatus.Restarting +function getInstalledPrimaryStatus(pkg: T.PackageDataEntry): PrimaryStatus { + if ( + Object.values(pkg.requestedActions).some( + r => r.active && r.request.severity === 'critical', + ) + ) { + return 'actionRequired' } else { - return status.main.status as any as PrimaryStatus + return pkg.status.main } } function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus { - return Object.values(depErrors).some(err => !!err) - ? DependencyStatus.Warning - : DependencyStatus.Satisfied + return Object.values(depErrors).some(err => !!err) ? 'warning' : 'satisfied' } -function getHealthStatus( - status: Status, - hasHealthChecks: boolean, -): HealthStatus | null { - if (status.main.status !== PackageMainStatus.Running || !status.main.health) { +function getHealthStatus(status: T.MainStatus): T.HealthStatus | null { + if (status.main !== 'running' || !status.main) { return null } - const values = Object.values(status.main.health) + const values = Object.values(status.health) if (values.some(h => h.result === 'failure')) { - return HealthStatus.Failure - } - - if (!values.length && hasHealthChecks) { - return HealthStatus.Waiting + return 'failure' } if (values.some(h => h.result === 'loading')) { - return HealthStatus.Loading + return 'loading' } - if (values.some(h => !h.result || h.result === 'starting')) { - return HealthStatus.Starting + if (values.some(h => h.result === 'starting')) { + return 'starting' } - return HealthStatus.Healthy + return 'success' } export interface StatusRendering { @@ -87,102 +71,94 @@ export interface StatusRendering { showDots?: boolean } -export enum PrimaryStatus { - // state - Installing = 'installing', - Updating = 'updating', - Removing = 'removing', - Restoring = 'restoring', - // status - Starting = 'starting', - Running = 'running', - Stopping = 'stopping', - Restarting = 'restarting', - Stopped = 'stopped', - BackingUp = 'backing-up', - // config - NeedsConfig = 'needs-config', -} - -export enum DependencyStatus { - Warning = 'warning', - Satisfied = 'satisfied', -} - -export enum HealthStatus { - Failure = 'failure', - Waiting = 'waiting', - Starting = 'starting', - Loading = 'loading', - Healthy = 'healthy', -} - -export const PrimaryRendering: Record = { - [PrimaryStatus.Installing]: { +export type PrimaryStatus = + | 'installing' + | 'updating' + | 'removing' + | 'restoring' + | 'starting' + | 'running' + | 'stopping' + | 'restarting' + | 'stopped' + | 'backingUp' + | 'actionRequired' + | 'error' + +export type DependencyStatus = 'warning' | 'satisfied' + +export const PrimaryRendering: Record = { + installing: { display: 'Installing', color: 'primary', showDots: true, }, - [PrimaryStatus.Updating]: { + updating: { display: 'Updating', color: 'primary', showDots: true, }, - [PrimaryStatus.Removing]: { + removing: { display: 'Removing', color: 'danger', showDots: true, }, - [PrimaryStatus.Restoring]: { + restoring: { display: 'Restoring', color: 'primary', showDots: true, }, - [PrimaryStatus.Stopping]: { + stopping: { display: 'Stopping', color: 'dark-shade', showDots: true, }, - [PrimaryStatus.Restarting]: { + restarting: { display: 'Restarting', color: 'tertiary', showDots: true, }, - [PrimaryStatus.Stopped]: { + stopped: { display: 'Stopped', color: 'dark-shade', showDots: false, }, - [PrimaryStatus.BackingUp]: { + backingUp: { display: 'Backing Up', color: 'primary', showDots: true, }, - [PrimaryStatus.Starting]: { + starting: { display: 'Starting', color: 'primary', showDots: true, }, - [PrimaryStatus.Running]: { + running: { display: 'Running', color: 'success', showDots: false, }, - [PrimaryStatus.NeedsConfig]: { - display: 'Needs Config', + actionRequired: { + display: 'Action Required', color: 'warning', showDots: false, }, + error: { + display: 'Service Launch Error', + color: 'danger', + showDots: false, + }, } -export const DependencyRendering: Record = { - [DependencyStatus.Warning]: { display: 'Issue', color: 'warning' }, - [DependencyStatus.Satisfied]: { display: 'Satisfied', color: 'success' }, +export const DependencyRendering: Record = { + warning: { display: 'Issue', color: 'warning' }, + satisfied: { display: 'Satisfied', color: 'success' }, } -export const HealthRendering: Record = { - [HealthStatus.Failure]: { display: 'Failure', color: 'danger' }, - [HealthStatus.Starting]: { display: 'Starting', color: 'primary' }, - [HealthStatus.Loading]: { display: 'Loading', color: 'primary' }, - [HealthStatus.Healthy]: { display: 'Healthy', color: 'success' }, +export const HealthRendering: Record = { + failure: { display: 'Failure', color: 'danger' }, + starting: { display: 'Starting', color: 'primary' }, + loading: { display: 'Loading', color: 'primary' }, + success: { display: 'Healthy', color: 'success' }, + disabled: { display: 'Disabled', color: 'dark' }, } diff --git a/web/projects/ui/src/app/services/standard-actions.service.ts b/web/projects/ui/src/app/services/standard-actions.service.ts new file mode 100644 index 000000000..664db822c --- /dev/null +++ b/web/projects/ui/src/app/services/standard-actions.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { hasCurrentDeps } from '../util/has-deps' +import { getAllPackages } from '../util/get-package-data' +import { PatchDB } from 'patch-db-client' +import { DataModel } from './patch-db/data-model' +import { AlertController, NavController } from '@ionic/angular' +import { ApiService } from './api/embassy-api.service' +import { ErrorService, LoadingService } from '@start9labs/shared' + +@Injectable({ + providedIn: 'root', +}) +export class StandardActionsService { + constructor( + private readonly patch: PatchDB, + private readonly api: ApiService, + private readonly alertCtrl: AlertController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, + private readonly navCtrl: NavController, + ) {} + + async rebuild(id: string) { + const loader = this.loader.open(`Rebuilding Container...`).subscribe() + + try { + await this.api.rebuildPackage({ id }) + this.navCtrl.navigateBack('/services/' + id) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + async tryUninstall(manifest: T.Manifest): Promise { + const { id, title, alerts } = manifest + + let message = + alerts.uninstall || + `Uninstalling ${title} will permanently delete its data` + + if (hasCurrentDeps(id, await getAllPackages(this.patch))) { + message = `${message}. Services that depend on ${title} will no longer work properly and may crash` + } + + const alert = await this.alertCtrl.create({ + header: 'Warning', + message, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Uninstall', + handler: () => { + this.uninstall(id) + }, + cssClass: 'enter-click', + }, + ], + cssClass: 'alert-warning-message', + }) + + await alert.present() + } + + private async uninstall(id: string) { + const loader = this.loader.open(`Beginning uninstall...`).subscribe() + + try { + await this.api.uninstallPackage({ id }) + this.api + .setDbValue(['ackInstructions', id], false) + .catch(e => console.error('Failed to mark instructions as unseen', e)) + this.navCtrl.navigateRoot('/services') + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } +} diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts new file mode 100644 index 000000000..9eb8caf0a --- /dev/null +++ b/web/projects/ui/src/app/services/state.service.ts @@ -0,0 +1,136 @@ +import { inject, Injectable } from '@angular/core' +import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router' +import { ALWAYS_TRUE_HANDLER } from '@taiga-ui/cdk' +import { TuiAlertService, TuiNotification } from '@taiga-ui/core' +import { + BehaviorSubject, + combineLatest, + concat, + EMPTY, + exhaustMap, + from, + merge, + Observable, + startWith, + Subject, + timer, +} from 'rxjs' +import { + catchError, + filter, + map, + shareReplay, + skip, + switchMap, + take, + takeUntil, + tap, +} from 'rxjs/operators' +import { RR } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { NetworkService } from 'src/app/services/network.service' + +const OPTIONS: IsActiveMatchOptions = { + paths: 'subset', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', +} + +@Injectable({ + providedIn: 'root', +}) +export class StateService extends Observable { + private readonly alerts = inject(TuiAlertService) + private readonly api = inject(ApiService) + private readonly router = inject(Router) + private readonly network$ = inject(NetworkService) + + private readonly single$ = new Subject() + + private readonly trigger$ = new BehaviorSubject(undefined) + private readonly poll$ = this.trigger$.pipe( + switchMap(() => + timer(0, 2000).pipe( + switchMap(() => + from(this.api.getState()).pipe(catchError(() => EMPTY)), + ), + take(1), + ), + ), + ) + + private readonly stream$ = merge(this.single$, this.poll$).pipe( + tap(state => { + switch (state) { + case 'initializing': + this.router.navigate(['initializing'], { replaceUrl: true }) + break + case 'error': + this.router.navigate(['diagnostic'], { replaceUrl: true }) + break + case 'running': + if ( + this.router.isActive('initializing', OPTIONS) || + this.router.isActive('diagnostic', OPTIONS) + ) { + this.router.navigate([''], { replaceUrl: true }) + } + + break + } + }), + startWith(null), + shareReplay(1), + ) + + private readonly alert = merge( + this.trigger$.pipe(skip(1)), + this.network$.pipe(filter(v => !v)), + ) + .pipe( + exhaustMap(() => + concat( + this.alerts + .open('Trying to reach server', { + label: 'State unknown', + autoClose: false, + status: TuiNotification.Error, + }) + .pipe( + takeUntil( + combineLatest([this.stream$, this.network$]).pipe( + filter(state => state.every(Boolean)), + ), + ), + ), + this.alerts.open('Connection restored', { + label: 'Server reached', + status: TuiNotification.Success, + }), + ), + ), + ) + .subscribe() + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + retrigger() { + this.trigger$.next() + } + + async syncState() { + const state = await this.api.getState() + this.single$.next(state) + } +} + +export function stateNot(state: RR.ServerState[]): CanActivateFn { + return () => + inject(StateService).pipe( + filter(current => !current || !state.includes(current)), + map(ALWAYS_TRUE_HANDLER), + ) +} diff --git a/web/projects/ui/src/app/services/storage.service.ts b/web/projects/ui/src/app/services/storage.service.ts index ec87864b5..e59eba439 100644 --- a/web/projects/ui/src/app/services/storage.service.ts +++ b/web/projects/ui/src/app/services/storage.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core' import { DOCUMENT } from '@angular/common' -const PREFIX = '_embassystorage/_embassykv/' +const PREFIX = '_startos/' @Injectable({ providedIn: 'root', @@ -15,16 +15,21 @@ export class StorageService { return JSON.parse(String(this.storage.getItem(`${PREFIX}${key}`))) } - set(key: string, value: T) { + set(key: string, value: any) { this.storage.setItem(`${PREFIX}${key}`, JSON.stringify(value)) } clear() { - Array.from( - { length: this.storage.length }, - (_, i) => this.storage.key(i) || '', - ) - .filter(key => key.startsWith(PREFIX)) - .forEach(key => this.storage.removeItem(key)) + this.storage.clear() + } + + migrate036() { + const oldPrefix = '_embassystorage/_embassykv/' + if (!!this.storage.getItem(`${oldPrefix}loggedInKey`)) { + const cache = this.storage.getItem(`${oldPrefix}patch-db-cache`) + this.clear() + this.set('loggedIn', true) + this.set('patchDB', cache) + } } } diff --git a/web/projects/ui/src/app/services/time-service.ts b/web/projects/ui/src/app/services/time-service.ts index 641144dae..3b58da744 100644 --- a/web/projects/ui/src/app/services/time-service.ts +++ b/web/projects/ui/src/app/services/time-service.ts @@ -32,7 +32,7 @@ export class TimeService { readonly now$ = combineLatest([ this.time$, - this.patch.watch$('server-info', 'ntp-synced'), + this.patch.watch$('serverInfo', 'ntpSynced'), ]).pipe( map(([time, synced]) => ({ value: time.now, diff --git a/web/projects/ui/src/app/services/ui-launcher.service.ts b/web/projects/ui/src/app/services/ui-launcher.service.ts index 55559bcd3..7c3475fc0 100644 --- a/web/projects/ui/src/app/services/ui-launcher.service.ts +++ b/web/projects/ui/src/app/services/ui-launcher.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@angular/core' import { WINDOW } from '@ng-web-apis/common' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { ConfigService } from './config.service' +import { T } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -12,7 +13,14 @@ export class UiLauncherService { private readonly config: ConfigService, ) {} - launch(pkg: PackageDataEntry): void { - this.windowRef.open(this.config.launchableURL(pkg), '_blank', 'noreferrer') + launch( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { + this.windowRef.open( + this.config.launchableAddress(interfaces, hosts), + '_blank', + 'noreferrer', + ) } } diff --git a/web/projects/ui/src/app/types/mapped-backup-target.ts b/web/projects/ui/src/app/types/mapped-backup-target.ts index 13b51d4b5..4b3610bec 100644 --- a/web/projects/ui/src/app/types/mapped-backup-target.ts +++ b/web/projects/ui/src/app/types/mapped-backup-target.ts @@ -1,5 +1,5 @@ export interface MappedBackupTarget { id: string - hasValidBackup: boolean + hasAnyBackup: boolean entry: T } diff --git a/web/projects/ui/src/app/types/progress-data.ts b/web/projects/ui/src/app/types/progress-data.ts deleted file mode 100644 index a05e475a1..000000000 --- a/web/projects/ui/src/app/types/progress-data.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ProgressData { - totalProgress: number - downloadProgress: number - validateProgress: number - unpackProgress: number - isComplete: boolean -} diff --git a/web/projects/ui/src/app/util/acme.ts b/web/projects/ui/src/app/util/acme.ts new file mode 100644 index 000000000..516472e11 --- /dev/null +++ b/web/projects/ui/src/app/util/acme.ts @@ -0,0 +1,21 @@ +export function toAcmeName(url: string | null): string | 'System CA' { + return knownACME.find(acme => acme.url === url)?.name || url || 'System CA' +} + +export function toAcmeUrl(name: string): string { + return knownACME.find(acme => acme.name === name)?.url || name +} + +export const knownACME: { + name: string + url: string +}[] = [ + { + name: `Let's Encrypt`, + url: 'https://acme-v02.api.letsencrypt.org/directory', + }, + { + name: `Let's Encrypt (Staging)`, + url: 'https://acme-staging-v02.api.letsencrypt.org/directory', + }, +] diff --git a/web/projects/ui/src/app/util/configBuilderToSpec.ts b/web/projects/ui/src/app/util/configBuilderToSpec.ts new file mode 100644 index 000000000..108bf9468 --- /dev/null +++ b/web/projects/ui/src/app/util/configBuilderToSpec.ts @@ -0,0 +1,9 @@ +import { ISB } from '@start9labs/start-sdk' + +export async function configBuilderToSpec( + builder: + | ISB.InputSpec, unknown> + | ISB.InputSpec, never>, +) { + return builder.build({} as any) +} diff --git a/web/projects/ui/src/app/util/dep-info.ts b/web/projects/ui/src/app/util/dep-info.ts new file mode 100644 index 000000000..cd98c5d4b --- /dev/null +++ b/web/projects/ui/src/app/util/dep-info.ts @@ -0,0 +1,30 @@ +import { + AllPackageData, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' + +export function getDepDetails( + pkg: PackageDataEntry, + allPkgs: AllPackageData, + depId: string, +) { + const { title, icon, versionRange } = pkg.currentDependencies[depId] + + if ( + allPkgs[depId] && + (allPkgs[depId].stateInfo.state === 'installed' || + allPkgs[depId].stateInfo.state === 'updating') + ) { + return { + title: allPkgs[depId].stateInfo.manifest!.title, + icon: allPkgs[depId].icon, + versionRange, + } + } else { + return { + title: title || depId, + icon: icon || 'assets/img/service-icons/fallback.png', + versionRange, + } + } +} diff --git a/web/projects/ui/src/app/util/dry-update.ts b/web/projects/ui/src/app/util/dry-update.ts index 9b7ba44a3..e9386df3c 100644 --- a/web/projects/ui/src/app/util/dry-update.ts +++ b/web/projects/ui/src/app/util/dry-update.ts @@ -1,17 +1,19 @@ -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { DataModel } from '../services/patch-db/data-model' +import { getManifest } from './get-package-data' export function dryUpdate( { id, version }: { id: string; version: string }, - pkgs: DataModel['package-data'], - emver: Emver, + pkgs: DataModel['packageData'], + exver: Exver, ): string[] { return Object.values(pkgs) .filter( pkg => - Object.keys(pkg.installed?.['current-dependencies'] || {}).some( + Object.keys(pkg.currentDependencies || {}).some( pkgId => pkgId === id, - ) && !emver.satisfies(version, pkg.manifest.dependencies[id].version), + ) && + !exver.satisfies(version, pkg.currentDependencies[id].versionRange), ) - .map(pkg => pkg.manifest.title) + .map(pkg => getManifest(pkg).title) } diff --git a/web/projects/ui/src/app/util/get-package-data.ts b/web/projects/ui/src/app/util/get-package-data.ts index 0645f3b3c..c4d0cc046 100644 --- a/web/projects/ui/src/app/util/get-package-data.ts +++ b/web/projects/ui/src/app/util/get-package-data.ts @@ -1,19 +1,59 @@ import { PatchDB } from 'patch-db-client' import { DataModel, + InstalledState, + InstallingState, PackageDataEntry, + UpdatingState, } from 'src/app/services/patch-db/data-model' import { firstValueFrom } from 'rxjs' +import { T } from '@start9labs/start-sdk' export async function getPackage( patch: PatchDB, id: string, ): Promise { - return firstValueFrom(patch.watch$('package-data', id)) + return firstValueFrom(patch.watch$('packageData', id)) } export async function getAllPackages( patch: PatchDB, -): Promise { - return firstValueFrom(patch.watch$('package-data')) +): Promise { + return firstValueFrom(patch.watch$('packageData')) +} + +export function getManifest(pkg: PackageDataEntry): T.Manifest { + if (isInstalled(pkg) || isRemoving(pkg)) return pkg.stateInfo.manifest + + return (pkg.stateInfo as InstallingState).installingInfo.newManifest +} + +export function isInstalled( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg.stateInfo.state === 'installed' +} + +export function isRemoving( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg.stateInfo.state === 'removing' +} + +export function isInstalling( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg.stateInfo.state === 'installing' +} + +export function isRestoring( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg.stateInfo.state === 'restoring' +} + +export function isUpdating( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg.stateInfo.state === 'updating' } diff --git a/web/projects/ui/src/app/util/get-package-info.ts b/web/projects/ui/src/app/util/get-package-info.ts index 14901f201..b21b42a37 100644 --- a/web/projects/ui/src/app/util/get-package-info.ts +++ b/web/projects/ui/src/app/util/get-package-info.ts @@ -1,15 +1,11 @@ import { PackageDataEntry } from '../services/patch-db/data-model' import { - DependencyStatus, - HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus, StatusRendering, } from '../services/pkg-status-rendering.service' -import { ProgressData } from 'src/app/types/progress-data' import { Subscription } from 'rxjs' -import { packageLoadingProgress } from './package-loading-progress' import { PkgDependencyErrors } from '../services/dep-error.service' export function getPackageInfo( @@ -23,16 +19,12 @@ export function getPackageInfo( entry, primaryRendering, primaryStatus: statuses.primary, - installProgress: packageLoadingProgress(entry['install-progress']), - error: - statuses.health === HealthStatus.Failure || - statuses.dependency === DependencyStatus.Warning, - warning: statuses.primary === PrimaryStatus.NeedsConfig, + error: statuses.health === 'failure' || statuses.dependency === 'warning', + warning: statuses.primary === 'actionRequired', transitioning: primaryRendering.showDots || - statuses.health === HealthStatus.Waiting || - statuses.health === HealthStatus.Loading || - statuses.health === HealthStatus.Starting, + statuses.health === 'loading' || + statuses.health === 'starting', } } @@ -40,7 +32,6 @@ export interface PkgInfo { entry: PackageDataEntry primaryRendering: StatusRendering primaryStatus: PrimaryStatus - installProgress: ProgressData | null error: boolean warning: boolean transitioning: boolean diff --git a/web/projects/ui/src/app/util/get-server-info.ts b/web/projects/ui/src/app/util/get-server-info.ts index 4500c4305..b5d5e819e 100644 --- a/web/projects/ui/src/app/util/get-server-info.ts +++ b/web/projects/ui/src/app/util/get-server-info.ts @@ -1,9 +1,10 @@ import { PatchDB } from 'patch-db-client' -import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { firstValueFrom } from 'rxjs' +import { T } from '@start9labs/start-sdk' export async function getServerInfo( patch: PatchDB, -): Promise { - return firstValueFrom(patch.watch$('server-info')) +): Promise { + return firstValueFrom(patch.watch$('serverInfo')) } diff --git a/web/projects/ui/src/app/util/has-deps.ts b/web/projects/ui/src/app/util/has-deps.ts index 94b3fa4cd..079990d1c 100644 --- a/web/projects/ui/src/app/util/has-deps.ts +++ b/web/projects/ui/src/app/util/has-deps.ts @@ -1,7 +1,8 @@ import { PackageDataEntry } from '../services/patch-db/data-model' -export function hasCurrentDeps(pkg: PackageDataEntry): boolean { - return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter( - depId => depId !== pkg.manifest.id, - ).length +export function hasCurrentDeps( + id: string, + pkgs: Record, +): boolean { + return !!Object.values(pkgs).some(pkg => !!pkg.currentDependencies[id]) } diff --git a/web/projects/ui/src/app/util/package-loading-progress.ts b/web/projects/ui/src/app/util/package-loading-progress.ts deleted file mode 100644 index 3c699dfd3..000000000 --- a/web/projects/ui/src/app/util/package-loading-progress.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { isEmptyObject } from '@start9labs/shared' -import { ProgressData } from 'src/app/types/progress-data' -import { InstallProgress } from '../services/patch-db/data-model' - -export function packageLoadingProgress( - loadData?: InstallProgress, -): ProgressData | null { - if (!loadData || isEmptyObject(loadData)) { - return null - } - - let { - downloaded, - validated, - unpacked, - size, - 'download-complete': downloadComplete, - 'validation-complete': validationComplete, - 'unpack-complete': unpackComplete, - } = loadData - - // only permit 100% when "complete" == true - size = size || 0 - downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0) - validated = validationComplete ? size : Math.max(validated - 1, 0) - unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0) - - const downloadWeight = 1 - const validateWeight = 0.2 - const unpackWeight = 0.7 - - const numerator = Math.floor( - downloadWeight * downloaded + - validateWeight * validated + - unpackWeight * unpacked, - ) - - const denominator = Math.floor( - size * (downloadWeight + validateWeight + unpackWeight), - ) - const totalProgress = Math.floor((100 * numerator) / denominator) - - return { - totalProgress, - downloadProgress: Math.floor((100 * downloaded) / size), - validateProgress: Math.floor((100 * validated) / size), - unpackProgress: Math.floor((100 * unpacked) / size), - isComplete: downloadComplete && validationComplete && unpackComplete, - } -} diff --git a/web/projects/ui/src/app/util/properties.util.ts b/web/projects/ui/src/app/util/properties.util.ts deleted file mode 100644 index 0d1d7e6f4..000000000 --- a/web/projects/ui/src/app/util/properties.util.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { applyOperation } from 'fast-json-patch' -import matches, { - Parser, - shape, - string, - literal, - boolean, - deferred, - dictionary, - anyOf, - number, - arrayOf, -} from 'ts-matches' - -type ValidVersion = 1 | 2 - -type PropertiesV1 = typeof matchPropertiesV1._TYPE -type PackagePropertiesV1 = PropertiesV1[] -type PackagePropertiesV2 = { - [name: string]: PackagePropertyString | PackagePropertyObject -} -type PackagePropertiesVersionedData = T extends 1 - ? PackagePropertiesV1 - : T extends 2 - ? PackagePropertiesV2 - : never - -type PackagePropertyString = typeof matchPackagePropertyString._TYPE - -export type PackagePropertiesVersioned = { - version: T - data: PackagePropertiesVersionedData -} -export type PackageProperties = PackagePropertiesV2 - -const matchPropertiesV1 = shape( - { - name: string, - value: string, - description: string, - copyable: boolean, - qr: boolean, - }, - ['description', 'copyable', 'qr'], - { copyable: false, qr: false } as const, -) - -const [matchPackagePropertiesV2, setPPV2] = deferred() -const matchPackagePropertyString = shape( - { - type: literal('string'), - description: string, - value: string, - copyable: boolean, - qr: boolean, - masked: boolean, - }, - ['description', 'copyable', 'qr', 'masked'], - { - copyable: false, - qr: false, - masked: false, - } as const, -) -const matchPackagePropertyObject = shape( - { - type: literal('object'), - value: matchPackagePropertiesV2, - description: string, - }, - ['description'], -) - -const matchPropertyV2 = anyOf( - matchPackagePropertyString, - matchPackagePropertyObject, -) -type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE -setPPV2(dictionary([string, matchPropertyV2])) - -const matchPackagePropertiesVersionedV1 = shape({ - version: number, - data: arrayOf(matchPropertiesV1), -}) -const matchPackagePropertiesVersionedV2 = shape({ - version: number, - data: dictionary([string, matchPropertyV2]), -}) - -export function parsePropertiesPermissive( - properties: unknown, - errorCallback: (err: Error) => any = console.warn, -): PackageProperties { - return matches(properties) - .when(matchPackagePropertiesVersionedV1, prop => - parsePropertiesV1Permissive(prop.data, errorCallback), - ) - .when(matchPackagePropertiesVersionedV2, prop => prop.data) - .when(matches.nill, {}) - .defaultToLazy(() => { - errorCallback(new TypeError(`value is not valid`)) - return {} - }) -} - -function parsePropertiesV1Permissive( - properties: unknown, - errorCallback: (err: Error) => any, -): PackageProperties { - if (!Array.isArray(properties)) { - errorCallback(new TypeError(`${properties} is not an array`)) - return {} - } - return properties.reduce( - (prev: PackagePropertiesV2, cur: unknown, idx: number) => { - const result = matchPropertiesV1.enumParsed(cur) - if ('value' in result) { - const value = result.value - prev[value.name] = { - type: 'string', - value: value.value, - description: value.description, - copyable: value.copyable, - qr: value.qr, - masked: false, - } - } else { - const error = result.error - const message = Parser.validatorErrorAsString(error) - const dataPath = error.keys.map(removeQuotes).join('/') - errorCallback(new Error(`/data/${idx}: ${message}`)) - if (dataPath) { - applyOperation(cur, { - op: 'replace', - path: `/${dataPath}`, - value: undefined, - }) - } - } - return prev - }, - {}, - ) -} - -const removeRegex = /('|")/ -function removeQuotes(x: string) { - while (removeRegex.test(x)) { - x = x.replace(removeRegex, '') - } - return x -} diff --git a/web/projects/ui/src/index.html b/web/projects/ui/src/index.html index 0248b7462..f3d8d40e3 100644 --- a/web/projects/ui/src/index.html +++ b/web/projects/ui/src/index.html @@ -12,19 +12,53 @@ /> - - + + + + + + - + + Start OS +

Loading

+ +
diff --git a/web/projects/ui/src/manifest.webmanifest b/web/projects/ui/src/manifest.webmanifest index fee3469fc..9dca24b6a 100644 --- a/web/projects/ui/src/manifest.webmanifest +++ b/web/projects/ui/src/manifest.webmanifest @@ -5,12 +5,18 @@ "background_color": "#1e1e1e", "display": "standalone", "scope": ".", - "start_url": "/?version=0351", - "id": "/?version=0351", + "start_url": "/?version=036", + "id": "/?version=036", "icons": [ { - "src": "assets/img/icon.png", - "sizes": "256x256", + "src": "/assets/icons/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/icons/web-app-manifest-512x512.png", + "sizes": "512x512", "type": "image/png", "purpose": "any" } diff --git a/web/projects/ui/src/polyfills.ts b/web/projects/ui/src/polyfills.ts index a392d45cf..813796e34 100644 --- a/web/projects/ui/src/polyfills.ts +++ b/web/projects/ui/src/polyfills.ts @@ -55,6 +55,9 @@ (window as any).global = window ; (window as any).process = { env: { DEBUG: undefined }, browser: true } +import { Buffer } from 'buffer' +window.Buffer = Buffer + import './zone-flags' /*************************************************************************************************** diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss index a00ca4ae2..34c76848b 100644 --- a/web/projects/ui/src/styles.scss +++ b/web/projects/ui/src/styles.scss @@ -110,6 +110,12 @@ $subheader-height: 48px; } } +.code-block { + background-color: rgb(69, 69, 69); + padding: 12px; + margin-bottom: 32px; +} + .center { display: block; margin: auto; @@ -347,4 +353,15 @@ p { svg:not(:root) { overflow: auto; -} \ No newline at end of file +} + +tui-dialog { + transform: translate3d(0, 0, 0); +} + +.g-buttons { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 24px; +} diff --git a/web/tsconfig.json b/web/tsconfig.json index 89f8e9548..6663ac431 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -22,7 +22,8 @@ "paths": { /* These paths are relative to each app base folder */ "@start9labs/marketplace": ["../marketplace/src/public-api"], - "@start9labs/shared": ["../shared/src/public-api"] + "@start9labs/shared": ["../shared/src/public-api"], + "path": ["../../node_modules/path-browserify"] }, "typeRoots": ["node_modules/@types"], "types": ["node"]

) -> Result { - let cfg = SetupContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?; + pub fn init( + webserver: &WebServer, + config: &ServerConfig, + ) -> Result { let (shutdown, _) = tokio::sync::broadcast::channel(1); - let datadir = cfg.datadir().to_owned(); Ok(Self(Arc::new(SetupContextSeed { - os_partitions: cfg.os_partitions, - config_path: path.as_ref().map(|p| p.as_ref().to_owned()), - migration_batch_rows: cfg.migration_batch_rows.unwrap_or(25000), - migration_prefetch_rows: cfg.migration_prefetch_rows.unwrap_or(100_000), - disable_encryption: cfg.disable_encryption, + webserver: webserver.acceptor_setter(), + config: config.clone(), + os_partitions: config.os_partitions.clone().ok_or_else(|| { + Error::new( + eyre!("missing required configuration: `os-partitions`"), + ErrorKind::NotFound, + ) + })?, + disable_encryption: config.disable_encryption.unwrap_or(false), + progress: FullProgressTracker::new(), + task: OnceCell::new(), + result: OnceCell::new(), shutdown, - datadir, - selected_v2_drive: RwLock::new(None), - cached_product_key: RwLock::new(None), - setup_status: RwLock::new(None), - setup_result: RwLock::new(None), + rpc_continuations: RpcContinuations::new(), }))) } #[instrument(skip_all)] - pub async fn db(&self, account: &AccountInfo) -> Result { - let db_path = self.datadir.join("main").join("embassy.db"); + pub async fn db(&self) -> Result { + let db_path = Path::new(MAIN_DATA).join("embassy.db"); let db = PatchDb::open(&db_path) .await .with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?; - if !db.exists(&::default()).await { - db.put(&::default(), &Database::init(account)) - .await?; - } Ok(db) } - #[instrument(skip_all)] - pub async fn secret_store(&self) -> Result { - init_postgres(&self.datadir).await?; - let secret_store = - PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root")) - .await?; - sqlx::migrate!() - .run(&secret_store) - .await - .with_kind(crate::ErrorKind::Database)?; - Ok(secret_store) + + pub fn run_setup(&self, f: F) -> Result<(), Error> + where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future> + Send, + { + let local_ctx = self.clone(); + self.task + .set( + tokio::spawn(async move { + local_ctx + .result + .get_or_init(|| async { + match f().await { + Ok(res) => { + tracing::info!("Setup complete!"); + Ok(res) + } + Err(e) => { + tracing::error!("Setup failed: {e}"); + tracing::debug!("{e:?}"); + Err(e) + } + } + }) + .await; + local_ctx.progress.complete(); + }) + .into(), + ) + .map_err(|_| { + if self.result.initialized() { + Error::new(eyre!("Setup already complete"), ErrorKind::InvalidRequest) + } else { + Error::new( + eyre!("Setup already in progress"), + ErrorKind::InvalidRequest, + ) + } + })?; + Ok(()) + } + + pub async fn progress(&self) -> SetupProgress { + use axum::extract::ws; + + let guid = Guid::new(); + let progress_tracker = self.progress.clone(); + let progress = progress_tracker.snapshot(); + self.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + if let Err(e) = async { + let mut stream = + progress_tracker.stream(Some(Duration::from_millis(100))); + loop { + tokio::select! { + progress = stream.next() => { + if let Some(progress) = progress { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + return ws.normal_close("complete").await; + } + } else { + return ws.normal_close("complete").await; + } + } + msg = ws.recv() => { + if msg.transpose().with_kind(ErrorKind::Network)?.is_none() { + return Ok(()) + } + } + } + } + } + .await + { + tracing::error!("Error in setup progress websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + + SetupProgress { progress, guid } + } +} + +impl AsRef for SetupContext { + fn as_ref(&self) -> &Jwk { + &*CURRENT_SECRET + } +} + +impl AsRef for SetupContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations } } diff --git a/core/startos/src/control.rs b/core/startos/src/control.rs index 58e39ac14..e831e07d6 100644 --- a/core/startos/src/control.rs +++ b/core/startos/src/control.rs @@ -1,92 +1,56 @@ +use clap::Parser; use color_eyre::eyre::eyre; -use rpc_toolkit::command; +use models::PackageId; +use serde::{Deserialize, Serialize}; use tracing::instrument; +use ts_rs::TS; use crate::context::RpcContext; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::status::MainStatus; -use crate::util::display_none; +use crate::rpc_continuations::Guid; use crate::Error; -#[command(display(display_none), metadata(sync_db = true))] -#[instrument(skip_all)] -pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> { - let peek = ctx.db.peek().await; - let version = peek - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .as_installed() - .or_not_found(&id)? - .as_manifest() - .as_version() - .de()?; +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ControlParams { + pub id: PackageId, +} - ctx.managers - .get(&(id, version)) +#[instrument(skip_all)] +pub async fn start(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { + ctx.services + .get(&id) .await - .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .start() - .await; + .as_ref() + .or_not_found(lazy_format!("Manager for {id}"))? + .start(Guid::new()) + .await?; Ok(()) } -#[command(display(display_none), metadata(sync_db = true))] -pub async fn stop(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result { - let peek = ctx.db.peek().await; - let version = peek - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .as_installed() - .or_not_found(&id)? - .as_manifest() - .as_version() - .de()?; - - let last_statuts = ctx - .db - .mutate(|v| { - v.as_package_data_mut() - .as_idx_mut(&id) - .and_then(|x| x.as_installed_mut()) - .ok_or_else(|| Error::new(eyre!("{} is not installed", id), ErrorKind::NotFound))? - .as_status_mut() - .as_main_mut() - .replace(&MainStatus::Stopping) - }) - .await?; - - ctx.managers - .get(&(id, version)) +pub async fn stop(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { + // TODO: why did this return last_status before? + ctx.services + .get(&id) .await + .as_ref() .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .stop() - .await; + .stop(Guid::new()) + .await?; - Ok(last_statuts) + Ok(()) } -#[command(display(display_none), metadata(sync_db = true))] -pub async fn restart(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> { - let peek = ctx.db.peek().await; - let version = peek - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .expect_as_installed()? - .as_manifest() - .as_version() - .de()?; - - ctx.managers - .get(&(id, version)) +pub async fn restart(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { + ctx.services + .get(&id) .await + .as_ref() .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .restart() - .await; + .restart(Guid::new()) + .await?; Ok(()) } diff --git a/core/startos/src/core/mod.rs b/core/startos/src/core/mod.rs deleted file mode 100644 index 7c2dbbb06..000000000 --- a/core/startos/src/core/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rpc_continuations; diff --git a/core/startos/src/core/rpc_continuations.rs b/core/startos/src/core/rpc_continuations.rs deleted file mode 100644 index 45a1c1b05..000000000 --- a/core/startos/src/core/rpc_continuations.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use futures::future::BoxFuture; -use futures::FutureExt; -use helpers::TimedResource; -use hyper::upgrade::Upgraded; -use hyper::{Body, Error as HyperError, Request, Response}; -use rand::RngCore; -use tokio::task::JoinError; -use tokio_tungstenite::WebSocketStream; - -use crate::{Error, ResultExt}; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] -pub struct RequestGuid = String>(Arc); -impl RequestGuid { - pub fn new() -> Self { - let mut buf = [0; 40]; - rand::thread_rng().fill_bytes(&mut buf); - RequestGuid(Arc::new(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - &buf, - ))) - } - - pub fn from(r: &str) -> Option { - if r.len() != 64 { - return None; - } - for c in r.chars() { - if !(c >= 'A' && c <= 'Z' || c >= '2' && c <= '7') { - return None; - } - } - Some(RequestGuid(Arc::new(r.to_owned()))) - } -} -#[test] -fn parse_guid() { - println!( - "{:?}", - RequestGuid::from(&format!("{}", RequestGuid::new())) - ) -} - -impl> std::fmt::Display for RequestGuid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - (&*self.0).as_ref().fmt(f) - } -} - -pub type RestHandler = Box< - dyn FnOnce(Request) -> BoxFuture<'static, Result, crate::Error>> + Send, ->; - -pub type WebSocketHandler = Box< - dyn FnOnce( - BoxFuture<'static, Result, HyperError>, JoinError>>, - ) -> BoxFuture<'static, Result<(), Error>> - + Send, ->; - -pub enum RpcContinuation { - Rest(TimedResource), - WebSocket(TimedResource), -} -impl RpcContinuation { - pub fn rest(handler: RestHandler, timeout: Duration) -> Self { - RpcContinuation::Rest(TimedResource::new(handler, timeout)) - } - pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self { - RpcContinuation::WebSocket(TimedResource::new(handler, timeout)) - } - pub fn is_timed_out(&self) -> bool { - match self { - RpcContinuation::Rest(a) => a.is_timed_out(), - RpcContinuation::WebSocket(a) => a.is_timed_out(), - } - } - pub async fn into_handler(self) -> Option { - match self { - RpcContinuation::Rest(handler) => handler.get().await, - RpcContinuation::WebSocket(handler) => { - if let Some(handler) = handler.get().await { - Some(Box::new( - |req: Request| -> BoxFuture<'static, Result, Error>> { - async move { - let (parts, body) = req.into_parts(); - let req = Request::from_parts(parts, body); - let (res, ws_fut) = hyper_ws_listener::create_ws(req) - .with_kind(crate::ErrorKind::Network)?; - if let Some(ws_fut) = ws_fut { - tokio::task::spawn(async move { - match handler(ws_fut.boxed()).await { - Ok(()) => (), - Err(e) => { - tracing::error!("WebSocket Closed: {}", e); - tracing::debug!("{:?}", e); - } - } - }); - } - - Ok(res) - } - .boxed() - }, - )) - } else { - None - } - } - } - } -} diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index 03ad94338..135153935 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -1,182 +1,60 @@ pub mod model; -pub mod package; pub mod prelude; -use std::future::Future; use std::path::PathBuf; use std::sync::Arc; - -use futures::{FutureExt, SinkExt, StreamExt}; -use patch_db::json_ptr::JsonPointer; -use patch_db::{Dump, Revision}; -use rpc_toolkit::command; -use rpc_toolkit::hyper::upgrade::Upgraded; -use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response}; +use std::time::Duration; + +use axum::extract::ws; +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use patch_db::json_ptr::{JsonPointer, ROOT}; +use patch_db::{DiffPatch, Dump, Revision}; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::sync::oneshot; -use tokio::task::JoinError; -use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; -use tokio_tungstenite::tungstenite::protocol::CloseFrame; -use tokio_tungstenite::tungstenite::Message; -use tokio_tungstenite::WebSocketStream; +use tokio::sync::mpsc::{self, UnboundedReceiver}; +use tokio::sync::watch; use tracing::instrument; +use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::middleware::auth::{HasValidSession, HashSessionToken}; use crate::prelude::*; -use crate::util::display_none; -use crate::util::serde::{display_serializable, IoFormat}; - -#[instrument(skip_all)] -async fn ws_handler< - WSFut: Future, HyperError>, JoinError>>, ->( - ctx: RpcContext, - session: Option<(HasValidSession, HashSessionToken)>, - ws_fut: WSFut, -) -> Result<(), Error> { - let (dump, sub) = ctx.db.dump_and_sub().await; - let mut stream = ws_fut - .await - .with_kind(ErrorKind::Network)? - .with_kind(ErrorKind::Unknown)?; - - if let Some((session, token)) = session { - let kill = subscribe_to_session_kill(&ctx, token).await; - send_dump(session, &mut stream, dump).await?; - - deal_with_messages(session, kill, sub, stream).await?; - } else { - stream - .close(Some(CloseFrame { - code: CloseCode::Error, - reason: "UNAUTHORIZED".into(), - })) - .await - .with_kind(ErrorKind::Network)?; - } - - Ok(()) -} - -async fn subscribe_to_session_kill( - ctx: &RpcContext, - token: HashSessionToken, -) -> oneshot::Receiver<()> { - let (send, recv) = oneshot::channel(); - let mut guard = ctx.open_authed_websockets.lock().await; - if !guard.contains_key(&token) { - guard.insert(token, vec![send]); - } else { - guard.get_mut(&token).unwrap().push(send); - } - recv -} - -#[instrument(skip_all)] -async fn deal_with_messages( - _has_valid_authentication: HasValidSession, - mut kill: oneshot::Receiver<()>, - mut sub: patch_db::Subscriber, - mut stream: WebSocketStream, -) -> Result<(), Error> { - let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(5)); - - loop { - futures::select! { - _ = (&mut kill).fuse() => { - tracing::info!("Closing WebSocket: Reason: Session Terminated"); - stream - .close(Some(CloseFrame { - code: CloseCode::Error, - reason: "UNAUTHORIZED".into(), - })) - .await - .with_kind(ErrorKind::Network)?; - return Ok(()) - } - new_rev = sub.recv().fuse() => { - let rev = new_rev.expect("UNREACHABLE: patch-db is dropped"); - stream - .send(Message::Text(serde_json::to_string(&rev).with_kind(ErrorKind::Serialization)?)) - .await - .with_kind(ErrorKind::Network)?; - } - message = stream.next().fuse() => { - let message = message.transpose().with_kind(ErrorKind::Network)?; - match message { - None => { - tracing::info!("Closing WebSocket: Stream Finished"); - return Ok(()) - } - _ => (), - } - } - // This is trying to give a health checks to the home to keep the ui alive. - _ = timer.tick().fuse() => { - stream - .send(Message::Ping(vec![])) - .await - .with_kind(crate::ErrorKind::Network)?; - } - } - } -} - -async fn send_dump( - _has_valid_authentication: HasValidSession, - stream: &mut WebSocketStream, - dump: Dump, -) -> Result<(), Error> { - stream - .send(Message::Text( - serde_json::to_string(&dump).with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - Ok(()) -} +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::util::net::WebSocketExt; +use crate::util::serde::{apply_expr, HandlerExtSerde}; -pub async fn subscribe(ctx: RpcContext, req: Request) -> Result, Error> { - let (parts, body) = req.into_parts(); - let session = match async { - let token = HashSessionToken::from_request_parts(&parts)?; - let session = HasValidSession::from_request_parts(&parts, &ctx).await?; - Ok::<_, Error>((session, token)) - } - .await - { - Ok(a) => Some(a), - Err(e) => { - if e.kind != ErrorKind::Authorization { - tracing::error!("Error Authenticating Websocket: {}", e); - tracing::debug!("{:?}", e); - } - None - } - }; - let req = Request::from_parts(parts, body); - let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(ErrorKind::Network)?; - if let Some(ws_fut) = ws_fut { - tokio::task::spawn(async move { - match ws_handler(ctx, session, ws_fut).await { - Ok(()) => (), - Err(e) => { - tracing::error!("WebSocket Closed: {}", e); - tracing::debug!("{:?}", e); - } - } - }); - } - - Ok(res) +lazy_static::lazy_static! { + static ref PUBLIC: JsonPointer = "/public".parse().unwrap(); } -#[command(subcommands(dump, put, apply))] -pub fn db() -> Result<(), RpcError> { - Ok(()) +pub fn db() -> ParentHandler { + ParentHandler::new() + .subcommand( + "dump", + from_fn_async(cli_dump) + .with_display_serializable() + .with_about("Filter/query db to display tables and records"), + ) + .subcommand("dump", from_fn_async(dump).no_cli()) + .subcommand( + "subscribe", + from_fn_async(subscribe) + .with_metadata("get_session", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "put", + put::().with_about("Command for adding UI record to db"), + ) + .subcommand( + "apply", + from_fn_async(cli_apply) + .no_display() + .with_about("Update a db record"), + ) + .subcommand("apply", from_fn_async(apply).no_cli()) } #[derive(Deserialize, Serialize)] @@ -186,145 +64,251 @@ pub enum RevisionsRes { Dump(Dump), } +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliDumpParams { + #[arg(long = "include-private", short = 'p')] + #[serde(default)] + include_private: bool, + path: Option, +} + #[instrument(skip_all)] async fn cli_dump( - ctx: CliContext, - _format: Option, - path: Option, + HandlerArgs { + context, + parent_method, + method, + params: CliDumpParams { + include_private, + path, + }, + .. + }: HandlerArgs, ) -> Result { let dump = if let Some(path) = path { - PatchDb::open(path).await?.dump().await + PatchDb::open(path).await?.dump(&ROOT).await } else { - rpc_toolkit::command_helpers::call_remote( - ctx, - "db.dump", - serde_json::json!({}), - std::marker::PhantomData::, - ) - .await? - .result? + let method = parent_method.into_iter().chain(method).join("."); + from_value::( + context + .call_remote::( + &method, + imbl_value::json!({ + "pointer": if include_private { + AsRef::::as_ref(&ROOT) + } else { + AsRef::::as_ref(&*PUBLIC) + } + }), + ) + .await?, + )? }; Ok(dump) } -#[command( - custom_cli(cli_dump(async, context(CliContext))), - display(display_serializable) -)] -pub async fn dump( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, - #[allow(unused_variables)] - #[arg] - path: Option, -) -> Result { - Ok(ctx.db.dump().await) +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DumpParams { + #[ts(type = "string | null")] + pointer: Option, } -fn apply_expr(input: jaq_core::Val, expr: &str) -> Result { - let (expr, errs) = jaq_core::parse::parse(expr, jaq_core::parse::main()); +pub async fn dump(ctx: RpcContext, DumpParams { pointer }: DumpParams) -> Result { + Ok(ctx.db.dump(pointer.as_ref().unwrap_or(&*PUBLIC)).await) +} - let Some(expr) = expr else { - return Err(Error::new( - eyre!("Failed to parse expression: {:?}", errs), - crate::ErrorKind::InvalidRequest, - )); - }; +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeParams { + #[ts(type = "string | null")] + pointer: Option, + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: Option, +} - let mut errs = Vec::new(); +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeRes { + #[ts(type = "{ id: number; value: unknown }")] + pub dump: Dump, + pub guid: Guid, +} - let mut defs = jaq_core::Definitions::core(); - for def in jaq_std::std() { - defs.insert(def, &mut errs); +struct DbSubscriber { + rev: u64, + sub: UnboundedReceiver, + sync_db: watch::Receiver, +} +impl DbSubscriber { + async fn recv(&mut self) -> Option { + loop { + tokio::select! { + rev = self.sub.recv() => { + if let Some(rev) = rev.as_ref() { + self.rev = rev.id; + } + return rev + } + _ = self.sync_db.changed() => { + let id = *self.sync_db.borrow(); + if id > self.rev { + match self.sub.try_recv() { + Ok(rev) => { + self.rev = rev.id; + return Some(rev) + } + Err(mpsc::error::TryRecvError::Disconnected) => { + return None + } + Err(mpsc::error::TryRecvError::Empty) => { + return Some(Revision { id, patch: DiffPatch::default() }) + } + } + } + } + } + } } +} - let filter = defs.finish(expr, Vec::new(), &mut errs); - - if !errs.is_empty() { - return Err(Error::new( - eyre!("Failed to compile expression: {:?}", errs), - crate::ErrorKind::InvalidRequest, - )); - }; - - let inputs = jaq_core::RcIter::new(std::iter::empty()); - let mut res_iter = filter.run(jaq_core::Ctx::new([], &inputs), input); - - let Some(res) = res_iter - .next() - .transpose() - .map_err(|e| eyre!("{e}")) - .with_kind(crate::ErrorKind::Deserialization)? - else { - return Err(Error::new( - eyre!("expr returned no results"), - crate::ErrorKind::InvalidRequest, - )); +pub async fn subscribe( + ctx: RpcContext, + SubscribeParams { pointer, session }: SubscribeParams, +) -> Result { + let (dump, sub) = ctx + .db + .dump_and_sub(pointer.unwrap_or_else(|| PUBLIC.clone())) + .await; + let mut sub = DbSubscriber { + rev: dump.id, + sub, + sync_db: ctx.sync_db.subscribe(), }; + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + |mut ws| async move { + if let Err(e) = async { + loop { + tokio::select! { + rev = sub.recv() => { + if let Some(rev) = rev { + ws.send(ws::Message::Text( + serde_json::to_string(&rev).with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } else { + return ws.normal_close("complete").await; + } + } + msg = ws.recv() => { + if msg.transpose().with_kind(ErrorKind::Network)?.is_none() { + return Ok(()) + } + } + } + } + } + .await + { + tracing::error!("Error in db websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; - if res_iter.next().is_some() { - return Err(Error::new( - eyre!("expr returned too many results"), - crate::ErrorKind::InvalidRequest, - )); - } + Ok(SubscribeRes { dump, guid }) +} - Ok(res) +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliApplyParams { + #[arg(long)] + allow_model_mismatch: bool, + expr: String, + path: Option, } #[instrument(skip_all)] -async fn cli_apply(ctx: CliContext, expr: String, path: Option) -> Result<(), RpcError> { +async fn cli_apply( + HandlerArgs { + context, + parent_method, + method, + params: + CliApplyParams { + allow_model_mismatch, + expr, + path, + }, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { if let Some(path) = path { PatchDb::open(path) .await? - .mutate(|db| { + .apply_function(|db| { let res = apply_expr( - serde_json::to_value(patch_db::Value::from(db.clone())) + serde_json::to_value(patch_db::Value::from(db)) .with_kind(ErrorKind::Deserialization)? .into(), &expr, )?; - db.ser( - &serde_json::from_value::(res.clone().into()).with_ctx( - |_| { - ( - crate::ErrorKind::Deserialization, - "result does not match database model", - ) - }, - )?, - ) + let value = if allow_model_mismatch { + serde_json::from_value::(res.clone().into()).with_ctx(|_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + })? + } else { + to_value( + &serde_json::from_value::(res.clone().into()).with_ctx( + |_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + }, + )?, + )? + }; + Ok::<_, Error>((value, ())) }) .await?; } else { - rpc_toolkit::command_helpers::call_remote( - ctx, - "db.apply", - serde_json::json!({ "expr": expr }), - std::marker::PhantomData::<()>, - ) - .await? - .result?; + let method = parent_method.into_iter().chain(method).join("."); + context + .call_remote::(&method, imbl_value::json!({ "expr": expr })) + .await?; } Ok(()) } -#[command( - custom_cli(cli_apply(async, context(CliContext))), - display(display_none) -)] -pub async fn apply( - #[context] ctx: RpcContext, - #[arg] expr: String, - #[allow(unused_variables)] - #[arg] - path: Option, -) -> Result<(), Error> { +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ApplyParams { + expr: String, +} + +pub async fn apply(ctx: RpcContext, ApplyParams { expr }: ApplyParams) -> Result<(), Error> { ctx.db .mutate(|db| { let res = apply_expr( @@ -346,22 +330,29 @@ pub async fn apply( .await } -#[command(subcommands(ui))] -pub fn put() -> Result<(), RpcError> { - Ok(()) +pub fn put() -> ParentHandler { + ParentHandler::new().subcommand( + "ui", + from_fn_async(ui) + .with_display_serializable() + .with_about("Add path and value to db") + .with_call_remote::(), + ) +} +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct UiParams { + #[ts(type = "string")] + pointer: JsonPointer, + #[ts(type = "any")] + value: Value, } -#[command(display(display_serializable))] +// #[command(display(display_serializable))] #[instrument(skip_all)] -pub async fn ui( - #[context] ctx: RpcContext, - #[arg] pointer: JsonPointer, - #[arg] value: Value, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result<(), Error> { - let ptr = "/ui" +pub async fn ui(ctx: RpcContext, UiParams { pointer, value, .. }: UiParams) -> Result<(), Error> { + let ptr = "/public/ui" .parse::() .with_kind(ErrorKind::Database)? + &pointer; diff --git a/core/startos/src/db/model.rs b/core/startos/src/db/model.rs deleted file mode 100644 index 344d5abb3..000000000 --- a/core/startos/src/db/model.rs +++ /dev/null @@ -1,530 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, Ipv6Addr}; -use std::sync::Arc; - -use chrono::{DateTime, Utc}; -use emver::VersionRange; -use imbl_value::InternedString; -use ipnet::{Ipv4Net, Ipv6Net}; -use isocountry::CountryCode; -use itertools::Itertools; -use models::{DataUrl, HealthCheckId, InterfaceId}; -use openssl::hash::MessageDigest; -use patch_db::{HasModel, Value}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use ssh_key::public::Ed25519PublicKey; - -use crate::account::AccountInfo; -use crate::config::spec::PackagePointerSpec; -use crate::install::progress::InstallProgress; -use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; -use crate::prelude::*; -use crate::s9pk::manifest::{Manifest, PackageId}; -use crate::status::Status; -use crate::util::cpupower::{Governor}; -use crate::util::Version; -use crate::version::{Current, VersionT}; -use crate::{ARCH, PLATFORM}; - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub struct Database { - pub server_info: ServerInfo, - pub package_data: AllPackageData, - pub ui: Value, -} -impl Database { - pub fn init(account: &AccountInfo) -> Self { - let lan_address = account.hostname.lan_address().parse().unwrap(); - Database { - server_info: ServerInfo { - arch: get_arch(), - platform: get_platform(), - id: account.server_id.clone(), - version: Current::new().semver().into(), - hostname: account.hostname.no_dot_host_name(), - last_backup: None, - last_wifi_region: None, - eos_version_compat: Current::new().compat().clone(), - lan_address, - tor_address: format!("https://{}", account.key.tor_address()) - .parse() - .unwrap(), - ip_info: BTreeMap::new(), - status_info: ServerStatus { - backup_progress: None, - updated: false, - update_progress: None, - shutting_down: false, - restarting: false, - }, - wifi: WifiInfo { - ssids: Vec::new(), - connected: None, - selected: None, - }, - unread_notification_count: 0, - connection_addresses: ConnectionAddresses { - tor: Vec::new(), - clearnet: Vec::new(), - }, - password_hash: account.password.clone(), - pubkey: ssh_key::PublicKey::from(Ed25519PublicKey::from(&account.key.ssh_key())) - .to_openssh() - .unwrap(), - ca_fingerprint: account - .root_ca_cert - .digest(MessageDigest::sha256()) - .unwrap() - .iter() - .map(|x| format!("{x:X}")) - .join(":"), - ntp_synced: false, - zram: true, - governor: None, - }, - package_data: AllPackageData::default(), - ui: serde_json::from_str(include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../web/patchdb-ui-seed.json" - ))) - .unwrap(), - } - } -} - -pub type DatabaseModel = Model; - -fn get_arch() -> InternedString { - (*ARCH).into() -} - -fn get_platform() -> InternedString { - (&*PLATFORM).into() -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerInfo { - #[serde(default = "get_arch")] - pub arch: InternedString, - #[serde(default = "get_platform")] - pub platform: InternedString, - pub id: String, - pub hostname: String, - pub version: Version, - pub last_backup: Option>, - /// Used in the wifi to determine the region to set the system to - pub last_wifi_region: Option, - pub eos_version_compat: VersionRange, - pub lan_address: Url, - pub tor_address: Url, - pub ip_info: BTreeMap, - #[serde(default)] - pub status_info: ServerStatus, - pub wifi: WifiInfo, - pub unread_notification_count: u64, - pub connection_addresses: ConnectionAddresses, - pub password_hash: String, - pub pubkey: String, - pub ca_fingerprint: String, - #[serde(default)] - pub ntp_synced: bool, - #[serde(default)] - pub zram: bool, - pub governor: Option, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct IpInfo { - pub ipv4_range: Option, - pub ipv4: Option, - pub ipv6_range: Option, - pub ipv6: Option, -} -impl IpInfo { - pub async fn for_interface(iface: &str) -> Result { - let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); - let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); - Ok(Self { - ipv4_range, - ipv4, - ipv6_range, - ipv6, - }) - } -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct BackupProgress { - pub complete: bool, -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerStatus { - pub backup_progress: Option>, - pub updated: bool, - pub update_progress: Option, - #[serde(default)] - pub shutting_down: bool, - #[serde(default)] - pub restarting: bool, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct UpdateProgress { - pub size: Option, - pub downloaded: u64, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct WifiInfo { - pub ssids: Vec, - pub selected: Option, - pub connected: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ServerSpecs { - pub cpu: String, - pub disk: String, - pub memory: String, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConnectionAddresses { - pub tor: Vec, - pub clearnet: Vec, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct AllPackageData(pub BTreeMap); -impl Map for AllPackageData { - type Key = PackageId; - type Value = PackageDataEntry; -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticFiles { - license: String, - instructions: String, - icon: String, -} -impl StaticFiles { - pub fn local(id: &PackageId, version: &Version, icon_type: &str) -> Self { - StaticFiles { - license: format!("/public/package-data/{}/{}/LICENSE.md", id, version), - instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version), - icon: format!("/public/package-data/{}/{}/icon.{}", id, version, icon_type), - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalling { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: Arc, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryUpdating { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, - pub install_progress: Arc, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRestoring { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: Arc, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRemoving { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub removing: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalled { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(tag = "state")] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub enum PackageDataEntry { - Installing(PackageDataEntryInstalling), - Updating(PackageDataEntryUpdating), - Restoring(PackageDataEntryRestoring), - Removing(PackageDataEntryRemoving), - Installed(PackageDataEntryInstalled), -} -impl Model { - pub fn expect_into_installed(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Installed(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Installed(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installed(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_into_removing(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Removing(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Removing(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Removing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn into_manifest(self) -> Model { - match self.into_match() { - PackageDataEntryMatchModel::Installing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Updating(a) => a.into_installed().into_manifest(), - PackageDataEntryMatchModel::Restoring(a) => a.into_manifest(), - PackageDataEntryMatchModel::Removing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Installed(a) => a.into_manifest(), - PackageDataEntryMatchModel::Error(_) => Model::from(Value::Null), - } - } - pub fn as_manifest(&self) -> &Model { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Updating(a) => a.as_installed().as_manifest(), - PackageDataEntryMatchModelRef::Restoring(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Removing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Installed(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Error(_) => (&Value::Null).into(), - } - } - pub fn into_installed(self) -> Option> { - match self.into_match() { - PackageDataEntryMatchModel::Installing(_) => None, - PackageDataEntryMatchModel::Updating(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Restoring(_) => None, - PackageDataEntryMatchModel::Removing(_) => None, - PackageDataEntryMatchModel::Installed(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Error(_) => None, - } - } - pub fn as_installed(&self) -> Option<&Model> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(_) => None, - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Restoring(_) => None, - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_installed_mut(&mut self) -> Option<&mut Model> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(_) => None, - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Restoring(_) => None, - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Error(_) => None, - } - } - pub fn as_install_progress(&self) -> Option<&Model>> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Restoring(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(_) => None, - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_install_progress_mut(&mut self) -> Option<&mut Model>> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Restoring(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(_) => None, - PackageDataEntryMatchModelMut::Error(_) => None, - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InstalledPackageInfo { - pub status: Status, - pub marketplace_url: Option, - #[serde(default)] - #[serde(with = "crate::util::serde::ed25519_pubkey")] - pub developer_key: ed25519_dalek::VerifyingKey, - pub manifest: Manifest, - pub last_backup: Option>, - pub dependency_info: BTreeMap, - pub current_dependents: CurrentDependents, - pub current_dependencies: CurrentDependencies, - pub interface_addresses: InterfaceAddressMap, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependents(pub BTreeMap); -impl CurrentDependents { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependents { - type Key = PackageId; - type Value = CurrentDependencyInfo; -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependencies(pub BTreeMap); -impl CurrentDependencies { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependencies { - type Key = PackageId; - type Value = CurrentDependencyInfo; -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticDependencyInfo { - pub title: String, - pub icon: DataUrl<'static>, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct CurrentDependencyInfo { - #[serde(default)] - pub pointers: BTreeSet, - pub health_checks: BTreeSet, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct InterfaceAddressMap(pub BTreeMap); -impl Map for InterfaceAddressMap { - type Key = InterfaceId; - type Value = InterfaceAddresses; -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InterfaceAddresses { - pub tor_address: Option, - pub lan_address: Option, -} diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs new file mode 100644 index 000000000..678f7e5fb --- /dev/null +++ b/core/startos/src/db/model/mod.rs @@ -0,0 +1,49 @@ +use std::collections::BTreeMap; + +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use crate::account::AccountInfo; +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::db::model::private::Private; +use crate::db::model::public::Public; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +pub mod package; +pub mod private; +pub mod public; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct Database { + pub public: Public, + pub private: Private, +} +impl Database { + pub fn init(account: &AccountInfo) -> Result { + Ok(Self { + public: Public::init(account)?, + private: Private { + key_store: KeyStore::new(account)?, + password: account.password.clone(), + ssh_privkey: Pem(account.ssh_key.clone()), + ssh_pubkeys: SshKeys::new(), + available_ports: AvailablePorts::new(), + sessions: Sessions::new(), + notifications: Notifications::new(), + cifs: CifsTargets::new(), + package_stores: BTreeMap::new(), + compat_s9pk_key: Pem(account.compat_s9pk_key.clone()), + }, // TODO + }) + } +} + +pub type DatabaseModel = Model; diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs new file mode 100644 index 000000000..6f1155311 --- /dev/null +++ b/core/startos/src/db/model/package.rs @@ -0,0 +1,537 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use chrono::{DateTime, Utc}; +use exver::VersionRange; +use imbl_value::InternedString; +use models::{ + ActionId, DataUrl, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId, + VersionString, +}; +use patch_db::json_ptr::JsonPointer; +use patch_db::HasModel; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::net::host::Hosts; +use crate::net::service_interface::ServiceInterface; +use crate::prelude::*; +use crate::progress::FullProgress; +use crate::s9pk::manifest::Manifest; +use crate::status::MainStatus; +use crate::util::serde::{is_partial_of, Pem}; + +#[derive(Debug, Default, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct AllPackageData(pub BTreeMap); +impl Map for AllPackageData { + type Key = PackageId; + type Value = PackageDataEntry; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ManifestPreference { + Old, + New, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "state")] +#[model = "Model"] +#[ts(export)] +pub enum PackageState { + Installing(InstallingState), + Restoring(InstallingState), + Updating(UpdatingState), + Installed(InstalledState), + Removing(InstalledState), +} +impl PackageState { + pub fn expect_installed(&self) -> Result<&InstalledState, Error> { + match self { + Self::Installed(a) => Ok(a), + _ => Err(Error::new( + eyre!( + "Package {} is not in installed state", + self.as_manifest(ManifestPreference::Old).id + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn expect_removing(&self) -> Result<&InstalledState, Error> { + match self { + Self::Removing(a) => Ok(a), + _ => Err(Error::new( + eyre!( + "Package {} is not in removing state", + self.as_manifest(ManifestPreference::Old).id + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn into_installing_info(self) -> Option { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn as_installing_info(&self) -> Option<&InstallingInfo> { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn as_installing_info_mut(&mut self) -> Option<&mut InstallingInfo> { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn into_manifest(self, preference: ManifestPreference) -> Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } + pub fn as_manifest(&self, preference: ManifestPreference) -> &Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } + pub fn as_manifest_mut(&mut self, preference: ManifestPreference) -> &mut Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } +} +impl Model { + pub fn expect_installed(&self) -> Result<&Model, Error> { + match self.as_match() { + PackageStateMatchModelRef::Installed(a) => Ok(a), + _ => Err(Error::new( + eyre!( + "Package {} is not in installed state", + self.as_manifest(ManifestPreference::Old).as_id().de()? + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn into_installing_info(self) -> Option> { + match self.into_match() { + PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => { + Some(s.into_installing_info()) + } + PackageStateMatchModel::Updating(s) => Some(s.into_installing_info()), + PackageStateMatchModel::Installed(_) | PackageStateMatchModel::Removing(_) => None, + PackageStateMatchModel::Error(_) => None, + } + } + pub fn as_installing_info(&self) -> Option<&Model> { + match self.as_match() { + PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => { + Some(s.as_installing_info()) + } + PackageStateMatchModelRef::Updating(s) => Some(s.as_installing_info()), + PackageStateMatchModelRef::Installed(_) | PackageStateMatchModelRef::Removing(_) => { + None + } + PackageStateMatchModelRef::Error(_) => None, + } + } + pub fn as_installing_info_mut(&mut self) -> Option<&mut Model> { + match self.as_match_mut() { + PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => { + Some(s.as_installing_info_mut()) + } + PackageStateMatchModelMut::Updating(s) => Some(s.as_installing_info_mut()), + PackageStateMatchModelMut::Installed(_) | PackageStateMatchModelMut::Removing(_) => { + None + } + PackageStateMatchModelMut::Error(_) => None, + } + } + pub fn into_manifest(self, preference: ManifestPreference) -> Model { + match self.into_match() { + PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => { + s.into_installing_info().into_new_manifest() + } + PackageStateMatchModel::Updating(s) if preference == ManifestPreference::Old => { + s.into_manifest() + } + PackageStateMatchModel::Updating(s) => s.into_installing_info().into_new_manifest(), + PackageStateMatchModel::Installed(s) | PackageStateMatchModel::Removing(s) => { + s.into_manifest() + } + PackageStateMatchModel::Error(_) => Value::Null.into(), + } + } + pub fn as_manifest(&self, preference: ManifestPreference) -> &Model { + match self.as_match() { + PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => { + s.as_installing_info().as_new_manifest() + } + PackageStateMatchModelRef::Updating(s) if preference == ManifestPreference::Old => { + s.as_manifest() + } + PackageStateMatchModelRef::Updating(s) => s.as_installing_info().as_new_manifest(), + PackageStateMatchModelRef::Installed(s) | PackageStateMatchModelRef::Removing(s) => { + s.as_manifest() + } + PackageStateMatchModelRef::Error(_) => (&Value::Null).into(), + } + } + pub fn as_manifest_mut( + &mut self, + preference: ManifestPreference, + ) -> Result<&mut Model, Error> { + Ok(match self.as_match_mut() { + PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => { + s.as_installing_info_mut().as_new_manifest_mut() + } + PackageStateMatchModelMut::Updating(s) if preference == ManifestPreference::Old => { + s.as_manifest_mut() + } + PackageStateMatchModelMut::Updating(s) => { + s.as_installing_info_mut().as_new_manifest_mut() + } + PackageStateMatchModelMut::Installed(s) | PackageStateMatchModelMut::Removing(s) => { + s.as_manifest_mut() + } + PackageStateMatchModelMut::Error(_) => { + return Err(Error::new( + eyre!("could not determine package state to get manifest"), + ErrorKind::Database, + )) + } + }) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct InstallingState { + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct UpdatingState { + pub manifest: Manifest, + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct InstalledState { + pub manifest: Manifest, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct InstallingInfo { + pub new_manifest: Manifest, + pub progress: FullProgress, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] +pub enum AllowedStatuses { + OnlyRunning, + OnlyStopped, + Any, +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct ActionMetadata { + /// A human-readable name + pub name: String, + /// A detailed description of what the action will do + pub description: String, + /// Presents as an alert prior to executing the action. Should be used sparingly but important if the action could have harmful, unintended consequences + pub warning: Option, + #[serde(default)] + /// One of: "enabled", "hidden", or { disabled: "" } + /// - "enabled" - the action is available be run + /// - "hidden" - the action cannot be seen or run + /// - { disabled: "example explanation" } means the action is visible but cannot be run. Replace "example explanation" with a reason why the action is disable to prevent user confusion. + pub visibility: ActionVisibility, + /// One of: "only-stopped", "only-running", "all" + /// - "only-stopped" - the action can only be run when the service is stopped + /// - "only-running" - the action can only be run when the service is running + /// - "any" - the action can only be run regardless of the service's status + pub allowed_statuses: AllowedStatuses, + pub has_input: bool, + /// If provided, this action will be nested under a header of this value, along with other actions of the same group + pub group: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] +#[serde(rename_all_fields = "camelCase")] +pub enum ActionVisibility { + Hidden, + Disabled(String), + Enabled, +} +impl Default for ActionVisibility { + fn default() -> Self { + Self::Enabled + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageDataEntry { + pub state_info: PackageState, + pub data_version: Option, + pub status: MainStatus, + #[ts(type = "string | null")] + pub registry: Option, + #[ts(type = "string")] + pub developer_key: Pem, + pub icon: DataUrl<'static>, + #[ts(type = "string | null")] + pub last_backup: Option>, + pub current_dependencies: CurrentDependencies, + pub actions: BTreeMap, + #[ts(as = "BTreeMap::")] + pub requested_actions: BTreeMap, + pub service_interfaces: BTreeMap, + pub hosts: Hosts, + #[ts(type = "string[]")] + pub store_exposed_dependents: Vec, +} +impl AsRef for PackageDataEntry { + fn as_ref(&self) -> &PackageDataEntry { + self + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct CurrentDependencies(pub BTreeMap); +impl CurrentDependencies { + pub fn map( + mut self, + transform: impl Fn( + BTreeMap, + ) -> BTreeMap, + ) -> Self { + self.0 = transform(self.0); + self + } +} +impl Map for CurrentDependencies { + type Key = PackageId; + type Value = CurrentDependencyInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct CurrentDependencyInfo { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, + #[serde(flatten)] + pub kind: CurrentDependencyKind, + #[ts(type = "string")] + pub version_range: VersionRange, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "kind")] +pub enum CurrentDependencyKind { + Exists, + #[serde(rename_all = "camelCase")] + Running { + #[serde(default)] + #[ts(type = "string[]")] + health_checks: BTreeSet, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[model = "Model"] +pub struct ActionRequestEntry { + pub request: ActionRequest, + pub active: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[model = "Model"] +pub struct ActionRequest { + pub package_id: PackageId, + pub action_id: ActionId, + #[serde(default)] + pub severity: ActionSeverity, + #[ts(optional)] + pub reason: Option, + #[ts(optional)] + pub when: Option, + #[ts(optional)] + pub input: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(export)] +pub enum ActionSeverity { + Critical, + Important, +} +impl Default for ActionSeverity { + fn default() -> Self { + ActionSeverity::Important + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ActionRequestTrigger { + #[serde(default)] + pub once: bool, + pub condition: ActionRequestCondition, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(export)] +pub enum ActionRequestCondition { + InputNotMatches, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "kind")] +pub enum ActionRequestInput { + Partial { + #[ts(type = "Record")] + value: Value, + }, +} +impl ActionRequestInput { + pub fn matches(&self, input: Option<&Value>) -> bool { + match self { + Self::Partial { value } => match input { + None => false, + Some(full) => is_partial_of(value, full), + }, + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct InterfaceAddressMap(pub BTreeMap); +impl Map for InterfaceAddressMap { + type Key = HostId; + type Value = InterfaceAddresses; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct InterfaceAddresses { + pub tor_address: Option, + pub lan_address: Option, +} diff --git a/core/startos/src/db/model/private.rs b/core/startos/src/db/model/private.rs new file mode 100644 index 000000000..2675b36d0 --- /dev/null +++ b/core/startos/src/db/model/private.rs @@ -0,0 +1,36 @@ +use std::collections::BTreeMap; + +use models::PackageId; +use patch_db::{HasModel, Value}; +use serde::{Deserialize, Serialize}; + +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct Private { + pub key_store: KeyStore, + pub password: String, // argon2 hash + #[serde(default = "generate_compat_key")] + pub compat_s9pk_key: Pem, + pub ssh_privkey: Pem, + pub ssh_pubkeys: SshKeys, + pub available_ports: AvailablePorts, + pub sessions: Sessions, + pub notifications: Notifications, + pub cifs: CifsTargets, + #[serde(default)] + pub package_stores: BTreeMap, +} + +pub fn generate_compat_key() -> Pem { + Pem(ed25519_dalek::SigningKey::generate(&mut rand::thread_rng())) +} diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs new file mode 100644 index 000000000..1f764e0c9 --- /dev/null +++ b/core/startos/src/db/model/public.rs @@ -0,0 +1,293 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{IpAddr, Ipv4Addr}; + +use chrono::{DateTime, Utc}; +use exver::{Version, VersionRange}; +use imbl_value::InternedString; +use ipnet::IpNet; +use isocountry::CountryCode; +use itertools::Itertools; +use models::PackageId; +use openssl::hash::MessageDigest; +use patch_db::{HasModel, Value}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::account::AccountInfo; +use crate::db::model::package::AllPackageData; +use crate::net::acme::AcmeProvider; +use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo}; +use crate::net::host::Host; +use crate::net::utils::ipv6_is_local; +use crate::net::vhost::AlpnInfo; +use crate::prelude::*; +use crate::progress::FullProgress; +use crate::system::SmtpValue; +use crate::util::cpupower::Governor; +use crate::util::lshw::LshwDevice; +use crate::util::serde::MaybeUtf8String; +use crate::version::{Current, VersionT}; +use crate::{ARCH, PLATFORM}; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Public { + pub server_info: ServerInfo, + pub package_data: AllPackageData, + #[ts(type = "unknown")] + pub ui: Value, +} +impl Public { + pub fn init(account: &AccountInfo) -> Result { + Ok(Self { + server_info: ServerInfo { + arch: get_arch(), + platform: get_platform(), + id: account.server_id.clone(), + version: Current::default().semver(), + hostname: account.hostname.no_dot_host_name(), + host: Host { + bindings: [( + 80, + BindInfo { + enabled: false, + options: BindOptions { + preferred_external_port: 80, + add_ssl: Some(AddSslOptions { + preferred_external_port: 443, + alpn: Some(AlpnInfo::Specified(vec![ + MaybeUtf8String("http/1.1".into()), + MaybeUtf8String("h2".into()), + ])), + }), + secure: None, + }, + net: NetInfo { + assigned_port: None, + assigned_ssl_port: Some(443), + public: false, + }, + }, + )] + .into_iter() + .collect(), + onions: account + .tor_keys + .iter() + .map(|k| k.public().get_onion_address()) + .collect(), + domains: BTreeMap::new(), + hostname_info: BTreeMap::new(), + }, + last_backup: None, + package_version_compat: Current::default().compat().clone(), + post_init_migration_todos: BTreeSet::new(), + network_interfaces: BTreeMap::new(), + acme: BTreeMap::new(), + status_info: ServerStatus { + backup_progress: None, + updated: false, + update_progress: None, + shutting_down: false, + restarting: false, + }, + wifi: WifiInfo::default(), + unread_notification_count: 0, + password_hash: account.password.clone(), + pubkey: ssh_key::PublicKey::from(&account.ssh_key) + .to_openssh() + .unwrap(), + ca_fingerprint: account + .root_ca_cert + .digest(MessageDigest::sha256()) + .unwrap() + .iter() + .map(|x| format!("{x:X}")) + .join(":"), + ntp_synced: false, + zram: true, + governor: None, + smtp: None, + ram: 0, + devices: Vec::new(), + }, + package_data: AllPackageData::default(), + ui: serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../web/patchdb-ui-seed.json" + ))) + .with_kind(ErrorKind::Deserialization)?, + }) + } +} + +fn get_arch() -> InternedString { + (*ARCH).into() +} + +fn get_platform() -> InternedString { + (&*PLATFORM).into() +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct ServerInfo { + #[serde(default = "get_arch")] + #[ts(type = "string")] + pub arch: InternedString, + #[serde(default = "get_platform")] + #[ts(type = "string")] + pub platform: InternedString, + pub id: String, + #[ts(type = "string")] + pub hostname: InternedString, + pub host: Host, + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub package_version_compat: VersionRange, + #[ts(type = "string[]")] + pub post_init_migration_todos: BTreeSet, + #[ts(type = "string | null")] + pub last_backup: Option>, + #[ts(as = "BTreeMap::")] + #[serde(default)] + pub network_interfaces: BTreeMap, + #[serde(default)] + pub acme: BTreeMap, + #[serde(default)] + pub status_info: ServerStatus, + pub wifi: WifiInfo, + #[ts(type = "number")] + pub unread_notification_count: u64, + pub password_hash: String, + pub pubkey: String, + pub ca_fingerprint: String, + #[serde(default)] + pub ntp_synced: bool, + #[serde(default)] + pub zram: bool, + pub governor: Option, + pub smtp: Option, + #[ts(type = "number")] + pub ram: u64, + pub devices: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct NetworkInterfaceInfo { + pub public: Option, + pub ip_info: Option, +} +impl NetworkInterfaceInfo { + pub fn public(&self) -> bool { + self.public.unwrap_or_else(|| { + !self.ip_info.as_ref().map_or(true, |ip_info| { + let ip4s = ip_info + .subnets + .iter() + .filter_map(|ipnet| { + if let IpAddr::V4(ip4) = ipnet.addr() { + Some(ip4) + } else { + None + } + }) + .collect::>(); + if !ip4s.is_empty() { + return ip4s.iter().all(|ip4| { + ip4.is_loopback() + || (ip4.is_private() && !ip4.octets().starts_with(&[10, 59])) // reserving 10.59 for public wireguard configurations + || ip4.is_link_local() + }); + } + ip_info.subnets.iter().all(|ipnet| { + if let IpAddr::V6(ip6) = ipnet.addr() { + ipv6_is_local(ip6) + } else { + true + } + }) + }) + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct IpInfo { + pub scope_id: u32, + pub device_type: Option, + #[ts(type = "string[]")] + pub subnets: BTreeSet, + pub wan_ip: Option, + #[ts(type = "string[]")] + pub ntp_servers: BTreeSet, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] +pub enum NetworkInterfaceType { + Ethernet, + Wireless, + Wireguard, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct AcmeSettings { + pub contact: Vec, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[model = "Model"] +#[ts(export)] +pub struct BackupProgress { + pub complete: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct ServerStatus { + pub backup_progress: Option>, + pub updated: bool, + pub update_progress: Option, + #[serde(default)] + pub shutting_down: bool, + #[serde(default)] + pub restarting: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct WifiInfo { + pub interface: Option, + pub ssids: BTreeSet, + pub selected: Option, + #[ts(type = "string | null")] + pub last_region: Option, +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ServerSpecs { + pub cpu: String, + pub disk: String, + pub memory: String, +} diff --git a/core/startos/src/db/package.rs b/core/startos/src/db/package.rs deleted file mode 100644 index fe6f93809..000000000 --- a/core/startos/src/db/package.rs +++ /dev/null @@ -1,22 +0,0 @@ -use models::Version; - -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; - -pub fn get_packages(db: Peeked) -> Result, Error> { - Ok(db - .as_package_data() - .keys()? - .into_iter() - .flat_map(|package_id| { - let version = db - .as_package_data() - .as_idx(&package_id)? - .as_manifest() - .as_version() - .de() - .ok()?; - Some((package_id, version)) - }) - .collect()) -} diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index 922a47500..419b356ef 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -1,17 +1,16 @@ use std::collections::BTreeMap; use std::marker::PhantomData; -use std::panic::UnwindSafe; +use std::str::FromStr; +use chrono::{DateTime, Utc}; +pub use imbl_value::Value; use patch_db::value::InternedString; -pub use patch_db::{HasModel, PatchDb, Value}; +pub use patch_db::{HasModel, PatchDb}; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::db::model::DatabaseModel; use crate::prelude::*; -pub type Peeked = Model; - pub fn to_value(value: &T) -> Result where T: Serialize, @@ -26,47 +25,7 @@ where patch_db::value::from_value(value).with_kind(ErrorKind::Deserialization) } -#[async_trait::async_trait] -pub trait PatchDbExt { - async fn peek(&self) -> DatabaseModel; - async fn mutate( - &self, - f: impl FnOnce(&mut DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result; - async fn map_mutate( - &self, - f: impl FnOnce(DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result; -} -#[async_trait::async_trait] -impl PatchDbExt for PatchDb { - async fn peek(&self) -> DatabaseModel { - DatabaseModel::from(self.dump().await.value) - } - async fn mutate( - &self, - f: impl FnOnce(&mut DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result { - Ok(self - .apply_function(|mut v| { - let model = <&mut DatabaseModel>::from(&mut v); - let res = f(model)?; - Ok::<_, Error>((v, res)) - }) - .await? - .1) - } - async fn map_mutate( - &self, - f: impl FnOnce(DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result { - Ok(DatabaseModel::from( - self.apply_function(|v| f(DatabaseModel::from(v)).map(|a| (a.into(), ()))) - .await? - .0, - )) - } -} +pub type TypedPatchDb = patch_db::TypedPatchDb; /// &mut Model <=> &mut Value #[repr(transparent)] @@ -90,12 +49,43 @@ impl Model { } } +impl Serialize for Model { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.value.serialize(serializer) + } +} + +impl<'de, T: Serialize + Deserialize<'de>> Deserialize<'de> for Model { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + Self::new(&T::deserialize(deserializer)?).map_err(D::Error::custom) + } +} + impl Model { pub fn replace(&mut self, value: &T) -> Result { let orig = self.de()?; self.ser(value)?; Ok(orig) } + pub fn mutate(&mut self, f: impl FnOnce(&mut T) -> Result) -> Result { + let mut orig = self.de()?; + let res = f(&mut orig)?; + self.ser(&orig)?; + Ok(res) + } + pub fn map_mutate(&mut self, f: impl FnOnce(T) -> Result) -> Result { + let orig = self.de()?; + let res = f(orig)?; + self.ser(&res)?; + Ok(res) + } } impl Clone for Model { fn clone(&self) -> Self { @@ -179,20 +169,38 @@ impl Model> { pub trait Map: DeserializeOwned + Serialize { type Key; type Value; + fn key_str(key: &Self::Key) -> Result, Error>; + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::intern(Self::key_str(key)?.as_ref())) + } } impl Map for BTreeMap +where + A: serde::Serialize + serde::de::DeserializeOwned + Ord + AsRef, + B: serde::Serialize + serde::de::DeserializeOwned, +{ + type Key = A; + type Value = B; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key.as_ref()) + } +} + +impl Map for BTreeMap, B> where A: serde::Serialize + serde::de::DeserializeOwned + Ord, B: serde::Serialize + serde::de::DeserializeOwned, { type Key = A; type Value = B; + fn key_str(key: &Self::Key) -> Result, Error> { + serde_json::to_string(key).with_kind(ErrorKind::Serialization) + } } impl Model where - T::Key: AsRef, T::Value: Serialize, { pub fn insert(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Error> { @@ -200,7 +208,7 @@ where let v = patch_db::value::to_value(value)?; match &mut self.value { Value::Object(o) => { - o.insert(InternedString::intern(key.as_ref()), v); + o.insert(T::key_string(key)?, v); Ok(()) } v => Err(patch_db::value::Error { @@ -210,13 +218,39 @@ where .into()), } } + pub fn upsert(&mut self, key: &T::Key, value: F) -> Result<&mut Model, Error> + where + F: FnOnce() -> Result, + { + use serde::ser::Error; + match &mut self.value { + Value::Object(o) => { + use patch_db::ModelExt; + let s = T::key_str(key)?; + let exists = o.contains_key(s.as_ref()); + let res = self.transmute_mut(|v| { + use patch_db::value::index::Index; + s.as_ref().index_or_insert(v) + }); + if !exists { + res.ser(&value()?)?; + } + Ok(res) + } + v => Err(patch_db::value::Error { + source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), + kind: patch_db::value::ErrorKind::Serialization, + } + .into()), + } + } pub fn insert_model(&mut self, key: &T::Key, value: Model) -> Result<(), Error> { use patch_db::ModelExt; use serde::ser::Error; let v = value.into_value(); match &mut self.value { Value::Object(o) => { - o.insert(InternedString::intern(key.as_ref()), v); + o.insert(T::key_string(key)?, v); Ok(()) } v => Err(patch_db::value::Error { @@ -230,25 +264,16 @@ where impl Model where - T::Key: DeserializeOwned + Ord + Clone, + T::Key: FromStr + Ord + Clone, + Error: From<::Err>, { pub fn keys(&self) -> Result, Error> { use serde::de::Error; - use serde::Deserialize; match &self.value { Value::Object(o) => o .keys() .cloned() - .map(|k| { - T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from(k)) - .map_err(|e| { - patch_db::value::Error { - kind: patch_db::value::ErrorKind::Deserialization, - source: e, - } - .into() - }) - }) + .map(|k| Ok(T::Key::from_str(&*k)?)) .collect(), v => Err(patch_db::value::Error { source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), @@ -261,19 +286,10 @@ where pub fn into_entries(self) -> Result)>, Error> { use patch_db::ModelExt; use serde::de::Error; - use serde::Deserialize; match self.value { Value::Object(o) => o .into_iter() - .map(|(k, v)| { - Ok(( - T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from( - k, - )) - .with_kind(ErrorKind::Deserialization)?, - Model::from_value(v), - )) - }) + .map(|(k, v)| Ok((T::Key::from_str(&*k)?, Model::from_value(v)))) .collect(), v => Err(patch_db::value::Error { source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), @@ -285,19 +301,10 @@ where pub fn as_entries(&self) -> Result)>, Error> { use patch_db::ModelExt; use serde::de::Error; - use serde::Deserialize; match &self.value { Value::Object(o) => o .iter() - .map(|(k, v)| { - Ok(( - T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from( - k.clone(), - )) - .with_kind(ErrorKind::Deserialization)?, - Model::value_as(v), - )) - }) + .map(|(k, v)| Ok((T::Key::from_str(&**k)?, Model::value_as(v)))) .collect(), v => Err(patch_db::value::Error { source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), @@ -309,19 +316,10 @@ where pub fn as_entries_mut(&mut self) -> Result)>, Error> { use patch_db::ModelExt; use serde::de::Error; - use serde::Deserialize; match &mut self.value { Value::Object(o) => o .iter_mut() - .map(|(k, v)| { - Ok(( - T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from( - k.clone(), - )) - .with_kind(ErrorKind::Deserialization)?, - Model::value_as_mut(v), - )) - }) + .map(|(k, v)| Ok((T::Key::from_str(&**k)?, Model::value_as_mut(v)))) .collect(), v => Err(patch_db::value::Error { source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), @@ -331,36 +329,48 @@ where } } } -impl Model -where - T::Key: AsRef, -{ +impl Model { + pub fn contains_key(&self, key: &T::Key) -> Result { + use serde::de::Error; + let s = T::key_str(key)?; + match &self.value { + Value::Object(o) => Ok(o.contains_key(s.as_ref())), + v => Err(patch_db::value::Error { + source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), + kind: patch_db::value::ErrorKind::Deserialization, + } + .into()), + } + } pub fn into_idx(self, key: &T::Key) -> Option> { use patch_db::ModelExt; + let s = T::key_str(key).ok()?; match &self.value { - Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute(|v| { + Value::Object(o) if o.contains_key(s.as_ref()) => Some(self.transmute(|v| { use patch_db::value::index::Index; - key.as_ref().index_into_owned(v).unwrap() + s.as_ref().index_into_owned(v).unwrap() })), _ => None, } } pub fn as_idx<'a>(&'a self, key: &T::Key) -> Option<&'a Model> { use patch_db::ModelExt; + let s = T::key_str(key).ok()?; match &self.value { - Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute_ref(|v| { + Value::Object(o) if o.contains_key(s.as_ref()) => Some(self.transmute_ref(|v| { use patch_db::value::index::Index; - key.as_ref().index_into(v).unwrap() + s.as_ref().index_into(v).unwrap() })), _ => None, } } pub fn as_idx_mut<'a>(&'a mut self, key: &T::Key) -> Option<&'a mut Model> { use patch_db::ModelExt; + let s = T::key_str(key).ok()?; match &mut self.value { - Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute_mut(|v| { + Value::Object(o) if o.contains_key(s.as_ref()) => Some(self.transmute_mut(|v| { use patch_db::value::index::Index; - key.as_ref().index_or_insert(v) + s.as_ref().index_or_insert(v) })), _ => None, } @@ -369,7 +379,7 @@ where use serde::ser::Error; match &mut self.value { Value::Object(o) => { - let v = o.remove(key.as_ref()); + let v = o.remove(T::key_str(key)?.as_ref()); Ok(v.map(patch_db::ModelExt::from_value)) } v => Err(patch_db::value::Error { @@ -380,3 +390,90 @@ where } } } + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct JsonKey(pub T); +impl From for JsonKey { + fn from(value: T) -> Self { + Self::new(value) + } +} +impl JsonKey { + pub fn new(value: T) -> Self { + Self(value) + } + pub fn unwrap(self) -> T { + self.0 + } + pub fn new_ref(value: &T) -> &Self { + unsafe { std::mem::transmute(value) } + } + pub fn new_mut(value: &mut T) -> &mut Self { + unsafe { std::mem::transmute(value) } + } +} +impl std::ops::Deref for JsonKey { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for JsonKey { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl Serialize for JsonKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + serde_json::to_string(&self.0) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} +// { "foo": "bar" } -> "{ \"foo\": \"bar\" }" +impl<'de, T: Serialize + DeserializeOwned> Deserialize<'de> for JsonKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let string = String::deserialize(deserializer)?; + Ok(Self( + serde_json::from_str(&string).map_err(D::Error::custom)?, + )) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WithTimeData { + pub created_at: DateTime, + pub updated_at: DateTime, + pub value: T, +} +impl WithTimeData { + pub fn new(value: T) -> Self { + let now = Utc::now(); + Self { + created_at: now, + updated_at: now, + value, + } + } +} +impl std::ops::Deref for WithTimeData { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.value + } +} +impl std::ops::DerefMut for WithTimeData { + fn deref_mut(&mut self) -> &mut Self::Target { + self.updated_at = Utc::now(); + &mut self.value + } +} diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index dfddecd93..3b55c8fc3 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -1,363 +1,44 @@ use std::collections::BTreeMap; -use std::time::Duration; -use color_eyre::eyre::eyre; -use emver::VersionRange; -use models::OptionExt; -use rand::SeedableRng; -use rpc_toolkit::command; +use imbl_value::InternedString; +use models::PackageId; use serde::{Deserialize, Serialize}; -use tracing::instrument; +use ts_rs::TS; -use crate::config::action::ConfigRes; -use crate::config::spec::PackagePointerSpec; -use crate::config::{not_found, Config, ConfigSpec, ConfigureContext}; -use crate::context::RpcContext; -use crate::db::model::{CurrentDependencies, Database}; use crate::prelude::*; -use crate::procedure::{NoOutput, PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::{Manifest, PackageId}; -use crate::status::DependencyConfigErrors; -use crate::util::serde::display_serializable; -use crate::util::{display_none, Version}; -use crate::volume::Volumes; +use crate::util::PathOrUrl; use crate::Error; -#[command(subcommands(configure))] -pub fn dependency() -> Result<(), Error> { - Ok(()) -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[model = "Model"] +#[ts(export)] pub struct Dependencies(pub BTreeMap); impl Map for Dependencies { type Key = PackageId; type Value = DepInfo; -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "type")] -pub enum DependencyRequirement { - OptIn { how: String }, - OptOut { how: String }, - Required, -} -impl DependencyRequirement { - pub fn required(&self) -> bool { - matches!(self, &DependencyRequirement::Required) + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) } } -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] #[model = "Model"] +#[ts(export)] pub struct DepInfo { - pub version: VersionRange, - pub requirement: DependencyRequirement, pub description: Option, - #[serde(default)] - pub config: Option, + pub optional: bool, + pub s9pk: Option, } -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] #[model = "Model"] -pub struct DependencyConfig { - check: PackageProcedure, - auto_configure: PackageProcedure, -} -impl DependencyConfig { - pub async fn check( - &self, - ctx: &RpcContext, - dependent_id: &PackageId, - dependent_version: &Version, - dependent_volumes: &Volumes, - dependency_id: &PackageId, - dependency_config: &Config, - ) -> Result, Error> { - Ok(self - .check - .sandboxed( - ctx, - dependent_id, - dependent_version, - dependent_volumes, - Some(dependency_config), - None, - ProcedureName::Check(dependency_id.clone()), - ) - .await? - .map_err(|(_, e)| e)) - } - pub async fn auto_configure( - &self, - ctx: &RpcContext, - dependent_id: &PackageId, - dependent_version: &Version, - dependent_volumes: &Volumes, - old: &Config, - ) -> Result { - self.auto_configure - .sandboxed( - ctx, - dependent_id, - dependent_version, - dependent_volumes, - Some(old), - None, - ProcedureName::AutoConfig(dependent_id.clone()), - ) - .await? - .map_err(|e| Error::new(eyre!("{}", e.1), crate::ErrorKind::AutoConfigure)) - } -} - -#[command( - subcommands(self(configure_impl(async)), configure_dry), - display(display_none) -)] -pub async fn configure( - #[arg(rename = "dependent-id")] dependent_id: PackageId, - #[arg(rename = "dependency-id")] dependency_id: PackageId, -) -> Result<(PackageId, PackageId), Error> { - Ok((dependent_id, dependency_id)) -} - -pub async fn configure_impl( - ctx: RpcContext, - (pkg_id, dep_id): (PackageId, PackageId), -) -> Result<(), Error> { - let breakages = BTreeMap::new(); - let overrides = Default::default(); - let ConfigDryRes { - old_config: _, - new_config, - spec: _, - } = configure_logic(ctx.clone(), (pkg_id, dep_id.clone())).await?; - - let configure_context = ConfigureContext { - breakages, - timeout: Some(Duration::from_secs(3).into()), - config: Some(new_config), - dry_run: false, - overrides, - }; - crate::config::configure(&ctx, &dep_id, configure_context).await?; - Ok(()) -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConfigDryRes { - pub old_config: Config, - pub new_config: Config, - pub spec: ConfigSpec, -} - -#[command(rename = "dry", display(display_serializable))] -#[instrument(skip_all)] -pub async fn configure_dry( - #[context] ctx: RpcContext, - #[parent_data] (pkg_id, dependency_id): (PackageId, PackageId), -) -> Result { - configure_logic(ctx, (pkg_id, dependency_id)).await -} - -pub async fn configure_logic( - ctx: RpcContext, - (pkg_id, dependency_id): (PackageId, PackageId), -) -> Result { - let db = ctx.db.peek().await; - let pkg = db - .as_package_data() - .as_idx(&pkg_id) - .or_not_found(&pkg_id)? - .as_installed() - .or_not_found(&pkg_id)?; - let pkg_version = pkg.as_manifest().as_version().de()?; - let pkg_volumes = pkg.as_manifest().as_volumes().de()?; - let dependency = db - .as_package_data() - .as_idx(&dependency_id) - .or_not_found(&dependency_id)? - .as_installed() - .or_not_found(&dependency_id)?; - let dependency_config_action = dependency - .as_manifest() - .as_config() - .de()? - .ok_or_else(|| not_found!("Manifest Config"))?; - let dependency_version = dependency.as_manifest().as_version().de()?; - let dependency_volumes = dependency.as_manifest().as_volumes().de()?; - let dependency = pkg - .as_manifest() - .as_dependencies() - .as_idx(&dependency_id) - .or_not_found(&dependency_id)?; - - let ConfigRes { - config: maybe_config, - spec, - } = dependency_config_action - .get( - &ctx, - &dependency_id, - &dependency_version, - &dependency_volumes, - ) - .await?; - - let old_config = if let Some(config) = maybe_config { - config - } else { - spec.gen( - &mut rand::rngs::StdRng::from_entropy(), - &Some(Duration::new(10, 0)), - )? - }; - - let new_config = dependency - .as_config() - .de()? - .ok_or_else(|| not_found!("Config"))? - .auto_configure - .sandboxed( - &ctx, - &pkg_id, - &pkg_version, - &pkg_volumes, - Some(&old_config), - None, - ProcedureName::AutoConfig(dependency_id.clone()), - ) - .await? - .map_err(|e| Error::new(eyre!("{}", e.1), crate::ErrorKind::AutoConfigure))?; - - Ok(ConfigDryRes { - old_config, - new_config, - spec, - }) -} - -#[instrument(skip_all)] -pub fn add_dependent_to_current_dependents_lists( - db: &mut Model, - dependent_id: &PackageId, - current_dependencies: &CurrentDependencies, -) -> Result<(), Error> { - for (dependency, dep_info) in ¤t_dependencies.0 { - if let Some(dependency_dependents) = db - .as_package_data_mut() - .as_idx_mut(dependency) - .and_then(|pde| pde.as_installed_mut()) - .map(|i| i.as_current_dependents_mut()) - { - dependency_dependents.insert(dependent_id, dep_info)?; - } - } - Ok(()) -} - -pub fn set_dependents_with_live_pointers_to_needs_config( - db: &mut Peeked, - id: &PackageId, -) -> Result, Error> { - let mut res = Vec::new(); - for (dep, info) in db - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_installed() - .or_not_found(id)? - .as_current_dependents() - .de()? - .0 - { - if info.pointers.iter().any(|ptr| match ptr { - // dependency id matches the package being uninstalled - PackagePointerSpec::TorAddress(ptr) => &ptr.package_id == id && &dep != id, - PackagePointerSpec::LanAddress(ptr) => &ptr.package_id == id && &dep != id, - // we never need to retarget these - PackagePointerSpec::TorKey(_) => false, - PackagePointerSpec::Config(_) => false, - }) { - let installed = db - .as_package_data_mut() - .as_idx_mut(&dep) - .or_not_found(&dep)? - .as_installed_mut() - .or_not_found(&dep)?; - let version = installed.as_manifest().as_version().de()?; - let configured = installed.as_status_mut().as_configured_mut(); - if configured.de()? { - configured.ser(&false)?; - res.push((dep, version)); - } - } - } - Ok(res) -} - -#[instrument(skip_all)] -pub async fn compute_dependency_config_errs( - ctx: &RpcContext, - db: &Peeked, - manifest: &Manifest, - current_dependencies: &CurrentDependencies, - dependency_config: &BTreeMap, -) -> Result { - let mut dependency_config_errs = BTreeMap::new(); - for (dependency, _dep_info) in current_dependencies - .0 - .iter() - .filter(|(dep_id, _)| dep_id != &&manifest.id) - { - // check if config passes dependency check - if let Some(cfg) = &manifest - .dependencies - .0 - .get(dependency) - .or_not_found(dependency)? - .config - { - if let Err(error) = cfg - .check( - ctx, - &manifest.id, - &manifest.version, - &manifest.volumes, - dependency, - &if let Some(config) = dependency_config.get(dependency) { - config.clone() - } else if let Some(manifest) = db - .as_package_data() - .as_idx(dependency) - .and_then(|pde| pde.as_installed()) - .map(|i| i.as_manifest().de()) - .transpose()? - { - if let Some(config) = &manifest.config { - config - .get(ctx, &manifest.id, &manifest.version, &manifest.volumes) - .await? - .config - .unwrap_or_default() - } else { - Config::default() - } - } else { - Config::default() - }, - ) - .await? - { - dependency_config_errs.insert(dependency.clone(), error); - } - } - } - Ok(DependencyConfigErrors(dependency_config_errs)) +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string")] + pub title: InternedString, } diff --git a/core/startos/src/developer/mod.rs b/core/startos/src/developer/mod.rs index 8722a4a11..4a2a4c3df 100644 --- a/core/startos/src/developer/mod.rs +++ b/core/startos/src/developer/mod.rs @@ -5,16 +5,14 @@ use std::path::Path; use ed25519::pkcs8::EncodePrivateKey; use ed25519::PublicKeyBytes; use ed25519_dalek::{SigningKey, VerifyingKey}; -use rpc_toolkit::command; use tracing::instrument; -use crate::context::SdkContext; -use crate::util::display_none; -use crate::{Error, ResultExt}; +use crate::context::CliContext; +use crate::prelude::*; +use crate::util::serde::Pem; -#[command(cli_only, blocking, display(display_none))] #[instrument(skip_all)] -pub fn init(#[context] ctx: SdkContext) -> Result<(), Error> { +pub fn init(ctx: CliContext) -> Result<(), Error> { if !ctx.developer_key_path.exists() { let parent = ctx.developer_key_path.parent().unwrap_or(Path::new("/")); if !parent.exists() { @@ -28,7 +26,8 @@ pub fn init(#[context] ctx: SdkContext) -> Result<(), Error> { secret_key: secret.to_bytes(), public_key: Some(PublicKeyBytes(VerifyingKey::from(&secret).to_bytes())), }; - let mut dev_key_file = File::create(&ctx.developer_key_path)?; + let mut dev_key_file = File::create(&ctx.developer_key_path) + .with_ctx(|_| (ErrorKind::Filesystem, ctx.developer_key_path.display()))?; dev_key_file.write_all( keypair_bytes .to_pkcs8_pem(base64ct::LineEnding::default()) @@ -49,7 +48,6 @@ pub fn init(#[context] ctx: SdkContext) -> Result<(), Error> { Ok(()) } -#[command(subcommands(crate::s9pk::verify, crate::config::verify_spec))] -pub fn verify() -> Result<(), Error> { - Ok(()) +pub fn pubkey(ctx: CliContext) -> Result, Error> { + Ok(Pem(ctx.developer_key()?.verifying_key())) } diff --git a/core/startos/src/diagnostic.rs b/core/startos/src/diagnostic.rs index aad95a5e5..73ef93125 100644 --- a/core/startos/src/diagnostic.rs +++ b/core/startos/src/diagnostic.rs @@ -1,72 +1,102 @@ use std::path::Path; use std::sync::Arc; -use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{ + from_fn, from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, +}; -use crate::context::DiagnosticContext; -use crate::disk::repair; +use crate::context::{CliContext, DiagnosticContext, RpcContext}; use crate::init::SYSTEM_REBUILD_PATH; -use crate::logs::{fetch_logs, LogResponse, LogSource}; use crate::shutdown::Shutdown; -use crate::util::display_none; -use crate::Error; +use crate::util::io::delete_file; +use crate::{Error, DATA_DIR}; -#[command(subcommands(error, logs, exit, restart, forget_disk, disk, rebuild))] -pub fn diagnostic() -> Result<(), Error> { - Ok(()) +pub fn diagnostic() -> ParentHandler { + ParentHandler::new() + .subcommand( + "error", + from_fn(error) + .with_about("Display diagnostic error") + .with_call_remote::(), + ) + .subcommand( + "logs", + crate::system::logs::().with_about("Display OS logs"), + ) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::) + .no_display() + .with_about("Display OS logs"), + ) + .subcommand( + "kernel-logs", + crate::system::kernel_logs::().with_about("Display kernel logs"), + ) + .subcommand( + "kernel-logs", + from_fn_async(crate::logs::cli_logs::) + .no_display() + .with_about("Display kernal logs"), + ) + .subcommand( + "restart", + from_fn(restart) + .no_display() + .with_about("Restart the server") + .with_call_remote::(), + ) + .subcommand( + "disk", + disk::().with_about("Command to remove disk from filesystem"), + ) + .subcommand( + "rebuild", + from_fn_async(rebuild) + .no_display() + .with_about("Teardown and rebuild service containers") + .with_call_remote::(), + ) } -#[command] -pub fn error(#[context] ctx: DiagnosticContext) -> Result, Error> { +// #[command] +pub fn error(ctx: DiagnosticContext) -> Result, Error> { Ok(ctx.error.clone()) } -#[command(rpc_only)] -pub async fn logs( - #[arg] limit: Option, - #[arg] cursor: Option, - #[arg] before: bool, -) -> Result { - Ok(fetch_logs(LogSource::System, limit, cursor, before).await?) -} - -#[command(display(display_none))] -pub fn exit(#[context] ctx: DiagnosticContext) -> Result<(), Error> { - ctx.shutdown.send(None).expect("receiver dropped"); - Ok(()) -} - -#[command(display(display_none))] -pub fn restart(#[context] ctx: DiagnosticContext) -> Result<(), Error> { +pub fn restart(ctx: DiagnosticContext) -> Result<(), Error> { ctx.shutdown - .send(Some(Shutdown { + .send(Shutdown { export_args: ctx .disk_guid .clone() - .map(|guid| (guid, ctx.datadir.clone())), + .map(|guid| (guid, Path::new(DATA_DIR).to_owned())), restart: true, - })) + }) .expect("receiver dropped"); Ok(()) } - -#[command(display(display_none))] -pub async fn rebuild(#[context] ctx: DiagnosticContext) -> Result<(), Error> { +pub async fn rebuild(ctx: DiagnosticContext) -> Result<(), Error> { tokio::fs::write(SYSTEM_REBUILD_PATH, b"").await?; restart(ctx) } -#[command(subcommands(forget_disk, repair))] -pub fn disk() -> Result<(), Error> { - Ok(()) +pub fn disk() -> ParentHandler { + ParentHandler::new() + .subcommand("forget", from_fn_async(forget_disk::).no_cli()) + .subcommand( + "forget", + CallRemoteHandler::::new( + from_fn_async(forget_disk::).no_display(), + ) + .no_display() + .with_about("Remove disk from filesystem"), + ) } -#[command(rename = "forget", display(display_none))] -pub async fn forget_disk() -> Result<(), Error> { - let disk_guid = Path::new("/media/embassy/config/disk.guid"); - if tokio::fs::metadata(disk_guid).await.is_ok() { - tokio::fs::remove_file(disk_guid).await?; - } +pub async fn forget_disk(_: C) -> Result<(), Error> { + delete_file("/media/startos/config/overlay/etc/hostname").await?; + delete_file("/media/startos/config/disk.guid").await?; Ok(()) } diff --git a/core/startos/src/disk/fsck/ext4.rs b/core/startos/src/disk/fsck/ext4.rs index 7bcbbc8b3..a068749fa 100644 --- a/core/startos/src/disk/fsck/ext4.rs +++ b/core/startos/src/disk/fsck/ext4.rs @@ -38,7 +38,7 @@ fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Err pub async fn e2fsck_aggressive( logicalname: impl AsRef + std::fmt::Debug, ) -> Result { - let undo_path = Path::new("/media/embassy/config") + let undo_path = Path::new("/media/startos/config") .join( logicalname .as_ref() diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index 74f6db73c..73aca4010 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -7,13 +7,13 @@ use tracing::instrument; use super::fsck::{RepairStrategy, RequiresReboot}; use super::util::pvscan; -use crate::disk::mount::filesystem::block_dev::mount; -use crate::disk::mount::filesystem::ReadWrite; +use crate::disk::mount::filesystem::block_dev::BlockDev; +use crate::disk::mount::filesystem::{FileSystem, ReadWrite}; use crate::disk::mount::util::unmount; use crate::util::Invoke; use crate::{Error, ErrorKind, ResultExt}; -pub const PASSWORD_PATH: &'static str = "/run/embassy/password"; +pub const PASSWORD_PATH: &'static str = "/run/startos/password"; pub const DEFAULT_PASSWORD: &'static str = "password"; pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8); @@ -64,10 +64,10 @@ where .await?; } let mut guid = format!( - "EMBASSY_{}", + "STARTOS_{}", base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - &rand::random::<[u8; 32]>(), + base32::Alphabet::Rfc4648 { padding: false }, + &rand::random::<[u8; 20]>(), ) ); if !encrypted { @@ -142,7 +142,9 @@ pub async fn create_fs>( .arg(&blockdev_path) .invoke(crate::ErrorKind::DiskManagement) .await?; - mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?; + BlockDev::new(&blockdev_path) + .mount(datadir.as_ref().join(name), ReadWrite) + .await?; Ok(()) } @@ -166,7 +168,7 @@ pub async fn create_all_fs>( #[instrument(skip_all)] pub async fn unmount_fs>(guid: &str, datadir: P, name: &str) -> Result<(), Error> { - unmount(datadir.as_ref().join(name)).await?; + unmount(datadir.as_ref().join(name), false).await?; if !guid.ends_with("_UNENC") { Command::new("cryptsetup") .arg("-q") @@ -217,7 +219,7 @@ pub async fn import>( if scan .values() .filter_map(|a| a.as_ref()) - .filter(|a| a.starts_with("EMBASSY_")) + .filter(|a| a.starts_with("STARTOS_") || a.starts_with("EMBASSY_")) .next() .is_none() { @@ -300,7 +302,7 @@ pub async fn mount_fs>( if !guid.ends_with("_UNENC") { // Backup LUKS header if e2fsck succeeded - let luks_folder = Path::new("/media/embassy/config/luks"); + let luks_folder = Path::new("/media/startos/config/luks"); tokio::fs::create_dir_all(luks_folder).await?; let tmp_luks_bak = luks_folder.join(format!(".{full_name}.luks.bak.tmp")); if tokio::fs::metadata(&tmp_luks_bak).await.is_ok() { @@ -318,7 +320,9 @@ pub async fn mount_fs>( tokio::fs::rename(&tmp_luks_bak, &luks_bak).await?; } - mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?; + BlockDev::new(&blockdev_path) + .mount(datadir.as_ref().join(name), ReadWrite) + .await?; Ok(reboot) } diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index 485d2570e..d1fbe282f 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -1,13 +1,13 @@ use std::path::{Path, PathBuf}; -use clap::ArgMatches; -use rpc_toolkit::command; +use itertools::Itertools; +use lazy_format::lazy_format; +use rpc_toolkit::{from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; use crate::disk::util::DiskInfo; -use crate::util::display_none; -use crate::util::serde::{display_serializable, IoFormat}; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::Error; pub mod fsck; @@ -16,10 +16,10 @@ pub mod mount; pub mod util; pub const BOOT_RW_PATH: &str = "/media/boot-rw"; -pub const REPAIR_DISK_PATH: &str = "/media/embassy/config/repair-disk"; +pub const REPAIR_DISK_PATH: &str = "/media/startos/config/repair-disk"; #[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct OsPartitionInfo { pub efi: Option, pub bios: Option, @@ -42,16 +42,34 @@ impl OsPartitionInfo { } } -#[command(subcommands(list, repair))] -pub fn disk() -> Result<(), Error> { - Ok(()) +pub fn disk() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_disk_info(handle.params, result)) + }) + .with_about("List disk info") + .with_call_remote::(), + ) + .subcommand("repair", from_fn_async(|_: C| repair()).no_cli()) + .subcommand( + "repair", + CallRemoteHandler::::new( + from_fn_async(|_: RpcContext| repair()) + .no_display() + .with_about("Repair disk in the event of corruption"), + ), + ) } -fn display_disk_info(info: Vec, matches: &ArgMatches) { +fn display_disk_info(params: WithIoFormat, args: Vec) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(info, matches); + if let Some(format) = params.format { + return display_serializable(format, args); } let mut table = Table::new(); @@ -60,9 +78,9 @@ fn display_disk_info(info: Vec, matches: &ArgMatches) { "LABEL", "CAPACITY", "USED", - "EMBASSY OS VERSION" + "STARTOS VERSION" ]); - for disk in info { + for disk in args { let row = row![ disk.logicalname.display(), "N/A", @@ -89,10 +107,18 @@ fn display_disk_info(info: Vec, matches: &ArgMatches) { } else { "N/A" }, - if let Some(eos) = part.embassy_os.as_ref() { - eos.version.as_str() + &if part.start_os.is_empty() { + "N/A".to_owned() + } else if part.start_os.len() == 1 { + part.start_os + .first_key_value() + .map(|(_, info)| info.version.to_string()) + .unwrap() } else { - "N/A" + part.start_os + .iter() + .map(|(id, info)| lazy_format!("{} ({})", info.version, id)) + .join(", ") }, ]; table.add_row(row); @@ -101,17 +127,11 @@ fn display_disk_info(info: Vec, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_disk_info))] -pub async fn list( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg] - format: Option, -) -> Result, Error> { +// #[command(display(display_disk_info))] +pub async fn list(ctx: RpcContext, _: Empty) -> Result, Error> { crate::disk::util::list(&ctx.os_partitions).await } -#[command(display(display_none))] pub async fn repair() -> Result<(), Error> { tokio::fs::write(REPAIR_DISK_PATH, b"").await?; Ok(()) diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index a19056241..3c6f97d91 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -1,51 +1,49 @@ use std::path::{Path, PathBuf}; +use std::sync::Arc; use color_eyre::eyre::eyre; use helpers::AtomicFile; +use models::PackageId; use tokio::io::AsyncWriteExt; use tracing::instrument; -use super::filesystem::ecryptfs::EcryptFS; use super::guard::{GenericMountGuard, TmpMountGuard}; -use super::util::{bind, unmount}; use crate::auth::check_password; use crate::backup::target::BackupInfo; +use crate::disk::mount::filesystem::backupfs::BackupFS; use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::util::EmbassyOsRecoveryInfo; -use crate::middleware::encrypt::{decrypt_slice, encrypt_slice}; -use crate::s9pk::manifest::PackageId; +use crate::disk::mount::guard::SubPath; +use crate::disk::util::StartOsRecoveryInfo; +use crate::util::crypto::{decrypt_slice, encrypt_slice}; use crate::util::serde::IoFormat; -use crate::util::FileLock; -use crate::volume::BACKUP_DIR; use crate::{Error, ErrorKind, ResultExt}; +#[derive(Clone, Debug)] pub struct BackupMountGuard { backup_disk_mount_guard: Option, encrypted_guard: Option, enc_key: String, - pub unencrypted_metadata: EmbassyOsRecoveryInfo, + unencrypted_metadata_path: PathBuf, + pub unencrypted_metadata: StartOsRecoveryInfo, pub metadata: BackupInfo, } impl BackupMountGuard { - fn backup_disk_path(&self) -> &Path { - if let Some(guard) = &self.backup_disk_mount_guard { - guard.as_ref() - } else { - unreachable!() - } - } - #[instrument(skip_all)] - pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result { - let backup_disk_path = backup_disk_mount_guard.as_ref(); - let unencrypted_metadata_path = - backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor"); - let mut unencrypted_metadata: EmbassyOsRecoveryInfo = + pub async fn mount( + backup_disk_mount_guard: G, + server_id: &str, + password: &str, + ) -> Result { + let backup_disk_path = backup_disk_mount_guard.path(); + let backup_dir = backup_disk_path.join("StartOSBackups").join(server_id); + let unencrypted_metadata_path = backup_dir.join("unencrypted-metadata.json"); + let crypt_path = backup_dir.join("crypt"); + let mut unencrypted_metadata: StartOsRecoveryInfo = if tokio::fs::metadata(&unencrypted_metadata_path) .await .is_ok() { - IoFormat::Cbor.from_slice( + IoFormat::Json.from_slice( &tokio::fs::read(&unencrypted_metadata_path) .await .with_ctx(|_| { @@ -56,6 +54,9 @@ impl BackupMountGuard { })?, )? } else { + if tokio::fs::metadata(&crypt_path).await.is_ok() { + tokio::fs::remove_dir_all(&crypt_path).await?; + } Default::default() }; let enc_key = if let (Some(hash), Some(wrapped_key)) = ( @@ -63,7 +64,7 @@ impl BackupMountGuard { unencrypted_metadata.wrapped_key.as_ref(), ) { let wrapped_key = - base32::decode(base32::Alphabet::RFC4648 { padding: true }, wrapped_key) + base32::decode(base32::Alphabet::Rfc4648 { padding: true }, wrapped_key) .ok_or_else(|| { Error::new( eyre!("failed to decode wrapped key"), @@ -74,7 +75,7 @@ impl BackupMountGuard { String::from_utf8(decrypt_slice(wrapped_key, password))? } else { base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 32]>()[..], ) }; @@ -91,12 +92,11 @@ impl BackupMountGuard { } if unencrypted_metadata.wrapped_key.is_none() { unencrypted_metadata.wrapped_key = Some(base32::encode( - base32::Alphabet::RFC4648 { padding: true }, + base32::Alphabet::Rfc4648 { padding: true }, &encrypt_slice(&enc_key, password), )); } - let crypt_path = backup_disk_path.join("EmbassyBackups/crypt"); if tokio::fs::metadata(&crypt_path).await.is_err() { tokio::fs::create_dir_all(&crypt_path).await.with_ctx(|_| { ( @@ -105,12 +105,15 @@ impl BackupMountGuard { ) })?; } - let encrypted_guard = - TmpMountGuard::mount(&EcryptFS::new(&crypt_path, &enc_key), ReadWrite).await?; + let encrypted_guard = TmpMountGuard::mount( + &BackupFS::new(&crypt_path, &enc_key, vec![(100000, 65536)]), + ReadWrite, + ) + .await?; - let metadata_path = encrypted_guard.as_ref().join("metadata.cbor"); + let metadata_path = encrypted_guard.path().join("metadata.json"); let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() { - IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| { + IoFormat::Json.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, metadata_path.display().to_string(), @@ -124,6 +127,7 @@ impl BackupMountGuard { backup_disk_mount_guard: Some(backup_disk_mount_guard), encrypted_guard: Some(encrypted_guard), enc_key, + unencrypted_metadata_path, unencrypted_metadata, metadata, }) @@ -139,58 +143,50 @@ impl BackupMountGuard { .with_kind(crate::ErrorKind::PasswordHashGeneration)?, ); self.unencrypted_metadata.wrapped_key = Some(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &encrypt_slice(&self.enc_key, new_password), )); Ok(()) } #[instrument(skip_all)] - pub async fn mount_package_backup( - &self, + pub async fn package_backup( + self: &Arc, id: &PackageId, - ) -> Result { - let lock = FileLock::new(Path::new(BACKUP_DIR).join(format!("{}.lock", id)), false).await?; - let mountpoint = Path::new(BACKUP_DIR).join(id); - bind(self.as_ref().join(id), &mountpoint, false).await?; - Ok(PackageBackupMountGuard { - mountpoint: Some(mountpoint), - lock: Some(lock), - }) + ) -> Result>, Error> { + let package_guard = SubPath::new(self.clone(), id); + let package_path = package_guard.path(); + if tokio::fs::metadata(&package_path).await.is_err() { + tokio::fs::create_dir_all(&package_path) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + package_path.display().to_string(), + ) + })?; + } + Ok(package_guard) } #[instrument(skip_all)] pub async fn save(&self) -> Result<(), Error> { - let metadata_path = self.as_ref().join("metadata.cbor"); - let backup_disk_path = self.backup_disk_path(); + let metadata_path = self.path().join("metadata.json"); let mut file = AtomicFile::new(&metadata_path, None::) .await .with_kind(ErrorKind::Filesystem)?; - file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?) + file.write_all(&IoFormat::Json.to_vec(&self.metadata)?) .await?; file.save().await.with_kind(ErrorKind::Filesystem)?; - let unencrypted_metadata_path = - backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor"); - let mut file = AtomicFile::new(&unencrypted_metadata_path, None::) + let mut file = AtomicFile::new(&self.unencrypted_metadata_path, None::) .await .with_kind(ErrorKind::Filesystem)?; - file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?) + file.write_all(&IoFormat::Json.to_vec(&self.unencrypted_metadata)?) .await?; file.save().await.with_kind(ErrorKind::Filesystem)?; Ok(()) } - #[instrument(skip_all)] - pub async fn unmount(mut self) -> Result<(), Error> { - if let Some(guard) = self.encrypted_guard.take() { - guard.unmount().await?; - } - if let Some(guard) = self.backup_disk_mount_guard.take() { - guard.unmount().await?; - } - Ok(()) - } - #[instrument(skip_all)] pub async fn save_and_unmount(self) -> Result<(), Error> { self.save().await?; @@ -198,14 +194,23 @@ impl BackupMountGuard { Ok(()) } } -impl AsRef for BackupMountGuard { - fn as_ref(&self) -> &Path { +impl GenericMountGuard for BackupMountGuard { + fn path(&self) -> &Path { if let Some(guard) = &self.encrypted_guard { - guard.as_ref() + guard.path() } else { unreachable!() } } + async fn unmount(mut self) -> Result<(), Error> { + if let Some(guard) = self.encrypted_guard.take() { + guard.unmount().await?; + } + if let Some(guard) = self.backup_disk_mount_guard.take() { + guard.unmount().await?; + } + Ok(()) + } } impl Drop for BackupMountGuard { fn drop(&mut self) { @@ -213,49 +218,10 @@ impl Drop for BackupMountGuard { let second = self.backup_disk_mount_guard.take(); tokio::spawn(async move { if let Some(guard) = first { - guard.unmount().await.unwrap(); + guard.unmount().await.log_err(); } if let Some(guard) = second { - guard.unmount().await.unwrap(); - } - }); - } -} - -pub struct PackageBackupMountGuard { - mountpoint: Option, - lock: Option, -} -impl PackageBackupMountGuard { - pub async fn unmount(mut self) -> Result<(), Error> { - if let Some(mountpoint) = self.mountpoint.take() { - unmount(&mountpoint).await?; - } - if let Some(lock) = self.lock.take() { - lock.unlock().await?; - } - Ok(()) - } -} -impl AsRef for PackageBackupMountGuard { - fn as_ref(&self) -> &Path { - if let Some(mountpoint) = &self.mountpoint { - mountpoint - } else { - unreachable!() - } - } -} -impl Drop for PackageBackupMountGuard { - fn drop(&mut self) { - let mountpoint = self.mountpoint.take(); - let lock = self.lock.take(); - tokio::spawn(async move { - if let Some(mountpoint) = mountpoint { - unmount(&mountpoint).await.unwrap(); - } - if let Some(lock) = lock { - lock.unlock().await.unwrap(); + guard.unmount().await.log_err(); } }); } diff --git a/core/startos/src/disk/mount/filesystem/backupfs.rs b/core/startos/src/disk/mount/filesystem/backupfs.rs new file mode 100644 index 000000000..254abde20 --- /dev/null +++ b/core/startos/src/disk/mount/filesystem/backupfs.rs @@ -0,0 +1,68 @@ +use std::borrow::Cow; +use std::fmt::{self, Display}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use digest::generic_array::GenericArray; +use digest::{Digest, OutputSizeUser}; +use sha2::Sha256; + +use super::FileSystem; +use crate::prelude::*; + +pub struct BackupFS, Password: fmt::Display> { + data_dir: DataDir, + password: Password, + idmapped_root: Vec<(u32, u32)>, +} +impl, Password: fmt::Display> BackupFS { + pub fn new(data_dir: DataDir, password: Password, idmapped_root: Vec<(u32, u32)>) -> Self { + BackupFS { + data_dir, + password, + idmapped_root, + } + } +} +impl + Send + Sync, Password: fmt::Display + Send + Sync> FileSystem + for BackupFS +{ + fn mount_type(&self) -> Option> { + Some("backup-fs") + } + fn mount_options(&self) -> impl IntoIterator { + [ + Cow::Owned(format!("password={}", self.password)), + Cow::Borrowed("file-size-padding=0.05"), + Cow::Borrowed("allow_other"), + ] + .into_iter() + .chain( + self.idmapped_root + .iter() + .map(|(root, range)| Cow::Owned(format!("idmapped-root={root}:{range}"))), + ) + } + async fn source(&self) -> Result>, Error> { + Ok(Some(&self.data_dir)) + } + async fn source_hash( + &self, + ) -> Result::OutputSize>, Error> { + let mut sha = Sha256::new(); + sha.update("BackupFS"); + sha.update( + tokio::fs::canonicalize(self.data_dir.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.data_dir.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + Ok(sha.finalize()) + } +} diff --git a/core/startos/src/disk/mount/filesystem/bind.rs b/core/startos/src/disk/mount/filesystem/bind.rs index 8799372e5..196e78a3d 100644 --- a/core/startos/src/disk/mount/filesystem/bind.rs +++ b/core/startos/src/disk/mount/filesystem/bind.rs @@ -1,14 +1,12 @@ use std::os::unix::ffi::OsStrExt; use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use sha2::Sha256; -use super::{FileSystem, MountType, ReadOnly}; -use crate::disk::mount::util::bind; -use crate::{Error, ResultExt}; +use super::FileSystem; +use crate::prelude::*; pub struct Bind> { src_dir: SrcDir, @@ -18,19 +16,16 @@ impl> Bind { Self { src_dir } } } -#[async_trait] impl + Send + Sync> FileSystem for Bind { - async fn mount + Send + Sync>( - &self, - mountpoint: P, - mount_type: MountType, - ) -> Result<(), Error> { - bind( - self.src_dir.as_ref(), - mountpoint, - matches!(mount_type, ReadOnly), - ) - .await + async fn source(&self) -> Result>, Error> { + Ok(Some(&self.src_dir)) + } + fn extra_args(&self) -> impl IntoIterator> { + ["--bind"] + } + async fn pre_mount(&self) -> Result<(), Error> { + tokio::fs::create_dir_all(self.src_dir.as_ref()).await?; + Ok(()) } async fn source_hash( &self, diff --git a/core/startos/src/disk/mount/filesystem/block_dev.rs b/core/startos/src/disk/mount/filesystem/block_dev.rs index e21f0c42d..7bbef2a77 100644 --- a/core/startos/src/disk/mount/filesystem/block_dev.rs +++ b/core/startos/src/disk/mount/filesystem/block_dev.rs @@ -1,33 +1,18 @@ use std::os::unix::ffi::OsStrExt; use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use serde::{Deserialize, Serialize}; use sha2::Sha256; +use ts_rs::TS; -use super::{FileSystem, MountType, ReadOnly}; -use crate::util::Invoke; -use crate::{Error, ResultExt}; +use super::FileSystem; +use crate::prelude::*; -pub async fn mount( - logicalname: impl AsRef, - mountpoint: impl AsRef, - mount_type: MountType, -) -> Result<(), Error> { - tokio::fs::create_dir_all(mountpoint.as_ref()).await?; - let mut cmd = tokio::process::Command::new("mount"); - cmd.arg(logicalname.as_ref()).arg(mountpoint.as_ref()); - if mount_type == ReadOnly { - cmd.arg("-o").arg("ro"); - } - cmd.invoke(crate::ErrorKind::Filesystem).await?; - Ok(()) -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(concrete(LogicalName = std::path::PathBuf))] pub struct BlockDev> { logicalname: LogicalName, } @@ -36,14 +21,9 @@ impl> BlockDev { BlockDev { logicalname } } } -#[async_trait] impl + Send + Sync> FileSystem for BlockDev { - async fn mount + Send + Sync>( - &self, - mountpoint: P, - mount_type: MountType, - ) -> Result<(), Error> { - mount(self.logicalname.as_ref(), mountpoint, mount_type).await + async fn source(&self) -> Result>, Error> { + Ok(Some(&self.logicalname)) } async fn source_hash( &self, diff --git a/core/startos/src/disk/mount/filesystem/cifs.rs b/core/startos/src/disk/mount/filesystem/cifs.rs index 91b477fcf..87509f150 100644 --- a/core/startos/src/disk/mount/filesystem/cifs.rs +++ b/core/startos/src/disk/mount/filesystem/cifs.rs @@ -2,16 +2,16 @@ use std::net::IpAddr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tokio::process::Command; use tracing::instrument; +use ts_rs::TS; use super::{FileSystem, MountType, ReadOnly}; -use crate::disk::mount::guard::TmpMountGuard; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::util::Invoke; use crate::Error; @@ -63,8 +63,8 @@ pub async fn mount_cifs( Ok(()) } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] pub struct Cifs { pub hostname: String, pub path: PathBuf, @@ -78,9 +78,8 @@ impl Cifs { Ok(()) } } -#[async_trait] impl FileSystem for Cifs { - async fn mount + Send + Sync>( + async fn mount + Send>( &self, mountpoint: P, mount_type: MountType, diff --git a/core/startos/src/disk/mount/filesystem/ecryptfs.rs b/core/startos/src/disk/mount/filesystem/ecryptfs.rs index 78570f49b..bf2dfe6c6 100644 --- a/core/startos/src/disk/mount/filesystem/ecryptfs.rs +++ b/core/startos/src/disk/mount/filesystem/ecryptfs.rs @@ -1,33 +1,17 @@ +use std::fmt::Display; use std::os::unix::ffi::OsStrExt; use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; +use lazy_format::lazy_format; use sha2::Sha256; +use tokio::process::Command; -use super::{FileSystem, MountType}; +use super::FileSystem; +use crate::disk::mount::filesystem::default_mount_command; +use crate::prelude::*; use crate::util::Invoke; -use crate::{Error, ResultExt}; - -pub async fn mount_ecryptfs, P1: AsRef>( - src: P0, - dst: P1, - key: &str, -) -> Result<(), Error> { - tokio::fs::create_dir_all(dst.as_ref()).await?; - tokio::process::Command::new("mount") - .arg("-t") - .arg("ecryptfs") - .arg(src.as_ref()) - .arg(dst.as_ref()) - .arg("-o") - // for more information `man ecryptfs` - .arg(format!("key=passphrase:passphrase_passwd={},ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_passthrough=n,ecryptfs_enable_filename_crypto=y,no_sig_cache", key)) - .input(Some(&mut std::io::Cursor::new(b"\n"))) - .invoke(crate::ErrorKind::Filesystem).await?; - Ok(()) -} pub struct EcryptFS, Key: AsRef> { encrypted_dir: EncryptedDir, @@ -38,16 +22,45 @@ impl, Key: AsRef> EcryptFS { EcryptFS { encrypted_dir, key } } } -#[async_trait] impl + Send + Sync, Key: AsRef + Send + Sync> FileSystem for EcryptFS { - async fn mount + Send + Sync>( + fn mount_type(&self) -> Option> { + Some("ecryptfs") + } + async fn source(&self) -> Result>, Error> { + Ok(Some(&self.encrypted_dir)) + } + fn mount_options(&self) -> impl IntoIterator { + [ + Box::new(lazy_format!( + "key=passphrase:passphrase_passwd={}", + self.key.as_ref() + )) as Box, + Box::new("ecryptfs_cipher=aes"), + Box::new("ecryptfs_key_bytes=32"), + Box::new("ecryptfs_passthrough=n"), + Box::new("ecryptfs_enable_filename_crypto=y"), + Box::new("no_sig_cache"), + ] + } + async fn mount + Send>( &self, mountpoint: P, - _mount_type: MountType, // ignored - inherited from parent fs + mount_type: super::MountType, ) -> Result<(), Error> { - mount_ecryptfs(self.encrypted_dir.as_ref(), mountpoint, self.key.as_ref()).await + self.pre_mount().await?; + tokio::fs::create_dir_all(mountpoint.as_ref()).await?; + Command::new("mount") + .args( + default_mount_command(self, mountpoint, mount_type) + .await? + .get_args(), + ) + .input(Some(&mut std::io::Cursor::new(b"\n"))) + .invoke(crate::ErrorKind::Filesystem) + .await?; + Ok(()) } async fn source_hash( &self, diff --git a/core/startos/src/disk/mount/filesystem/efivarfs.rs b/core/startos/src/disk/mount/filesystem/efivarfs.rs index ad9d79941..4961b4716 100644 --- a/core/startos/src/disk/mount/filesystem/efivarfs.rs +++ b/core/startos/src/disk/mount/filesystem/efivarfs.rs @@ -1,33 +1,19 @@ use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use sha2::Sha256; -use super::{FileSystem, MountType, ReadOnly}; -use crate::util::Invoke; -use crate::Error; +use super::FileSystem; +use crate::prelude::*; pub struct EfiVarFs; -#[async_trait] impl FileSystem for EfiVarFs { - async fn mount + Send + Sync>( - &self, - mountpoint: P, - mount_type: MountType, - ) -> Result<(), Error> { - tokio::fs::create_dir_all(mountpoint.as_ref()).await?; - let mut cmd = tokio::process::Command::new("mount"); - cmd.arg("-t") - .arg("efivarfs") - .arg("efivarfs") - .arg(mountpoint.as_ref()); - if mount_type == ReadOnly { - cmd.arg("-o").arg("ro"); - } - cmd.invoke(crate::ErrorKind::Filesystem).await?; - Ok(()) + fn mount_type(&self) -> Option> { + Some("efivarfs") + } + async fn source(&self) -> Result>, Error> { + Ok(Some("efivarfs")) } async fn source_hash( &self, diff --git a/core/startos/src/disk/mount/filesystem/httpdirfs.rs b/core/startos/src/disk/mount/filesystem/httpdirfs.rs index fda437ec3..df9416774 100644 --- a/core/startos/src/disk/mount/filesystem/httpdirfs.rs +++ b/core/startos/src/disk/mount/filesystem/httpdirfs.rs @@ -1,6 +1,5 @@ use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use reqwest::Url; @@ -23,7 +22,7 @@ pub async fn mount_httpdirfs(url: &Url, mountpoint: impl AsRef) -> Result< } #[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct HttpDirFS { url: Url, } @@ -32,9 +31,8 @@ impl HttpDirFS { HttpDirFS { url } } } -#[async_trait] impl FileSystem for HttpDirFS { - async fn mount + Send + Sync>( + async fn mount + Send>( &self, mountpoint: P, _mount_type: MountType, diff --git a/core/startos/src/disk/mount/filesystem/idmapped.rs b/core/startos/src/disk/mount/filesystem/idmapped.rs new file mode 100644 index 000000000..dc6a6e9ab --- /dev/null +++ b/core/startos/src/disk/mount/filesystem/idmapped.rs @@ -0,0 +1,88 @@ +use std::ffi::OsStr; +use std::fmt::Display; +use std::path::Path; + +use digest::generic_array::GenericArray; +use digest::{Digest, OutputSizeUser}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use tokio::process::Command; + +use super::{FileSystem, MountType}; +use crate::disk::mount::filesystem::default_mount_command; +use crate::prelude::*; +use crate::util::Invoke; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IdMapped { + filesystem: Fs, + from_id: u32, + to_id: u32, + range: u32, +} +impl IdMapped { + pub fn new(filesystem: Fs, from_id: u32, to_id: u32, range: u32) -> Self { + Self { + filesystem, + from_id, + to_id, + range, + } + } +} +impl FileSystem for IdMapped { + fn mount_type(&self) -> Option> { + self.filesystem.mount_type() + } + fn extra_args(&self) -> impl IntoIterator> { + self.filesystem.extra_args() + } + fn mount_options(&self) -> impl IntoIterator { + self.filesystem + .mount_options() + .into_iter() + .map(|a| Box::new(a) as Box) + .chain(std::iter::once(Box::new(lazy_format!( + "X-mount.idmap=b:{}:{}:{}", + self.from_id, + self.to_id, + self.range, + )) as Box)) + } + async fn source(&self) -> Result>, Error> { + self.filesystem.source().await + } + async fn pre_mount(&self) -> Result<(), Error> { + self.filesystem.pre_mount().await + } + async fn mount + Send>( + &self, + mountpoint: P, + mount_type: MountType, + ) -> Result<(), Error> { + self.pre_mount().await?; + tokio::fs::create_dir_all(mountpoint.as_ref()).await?; + Command::new("mount.next") + .args( + default_mount_command(self, mountpoint, mount_type) + .await? + .get_args(), + ) + .invoke(ErrorKind::Filesystem) + .await?; + + Ok(()) + } + async fn source_hash( + &self, + ) -> Result::OutputSize>, Error> { + let mut sha = Sha256::new(); + sha.update("IdMapped"); + sha.update(self.filesystem.source_hash().await?); + sha.update(u32::to_be_bytes(self.from_id)); + sha.update(u32::to_be_bytes(self.to_id)); + sha.update(u32::to_be_bytes(self.range)); + Ok(sha.finalize()) + } +} diff --git a/core/startos/src/disk/mount/filesystem/label.rs b/core/startos/src/disk/mount/filesystem/label.rs index b1e4f7213..57312bf13 100644 --- a/core/startos/src/disk/mount/filesystem/label.rs +++ b/core/startos/src/disk/mount/filesystem/label.rs @@ -1,28 +1,11 @@ use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use sha2::Sha256; -use super::{FileSystem, MountType, ReadOnly}; -use crate::util::Invoke; -use crate::Error; - -pub async fn mount_label( - label: &str, - mountpoint: impl AsRef, - mount_type: MountType, -) -> Result<(), Error> { - tokio::fs::create_dir_all(mountpoint.as_ref()).await?; - let mut cmd = tokio::process::Command::new("mount"); - cmd.arg("-L").arg(label).arg(mountpoint.as_ref()); - if mount_type == ReadOnly { - cmd.arg("-o").arg("ro"); - } - cmd.invoke(crate::ErrorKind::Filesystem).await?; - Ok(()) -} +use super::FileSystem; +use crate::prelude::*; pub struct Label> { label: S, @@ -32,14 +15,12 @@ impl> Label { Label { label } } } -#[async_trait] impl + Send + Sync> FileSystem for Label { - async fn mount + Send + Sync>( - &self, - mountpoint: P, - mount_type: MountType, - ) -> Result<(), Error> { - mount_label(self.label.as_ref(), mountpoint, mount_type).await + fn extra_args(&self) -> impl IntoIterator> { + ["-L", self.label.as_ref()] + } + async fn source(&self) -> Result>, Error> { + Ok(None::<&Path>) } async fn source_hash( &self, diff --git a/core/startos/src/disk/mount/filesystem/loop_dev.rs b/core/startos/src/disk/mount/filesystem/loop_dev.rs new file mode 100644 index 000000000..1728ef2c7 --- /dev/null +++ b/core/startos/src/disk/mount/filesystem/loop_dev.rs @@ -0,0 +1,63 @@ +use std::fmt::Display; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use digest::generic_array::GenericArray; +use digest::{Digest, OutputSizeUser}; +use lazy_format::lazy_format; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use super::FileSystem; +use crate::prelude::*; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoopDev> { + logicalname: LogicalName, + offset: u64, + size: u64, +} +impl> LoopDev { + pub fn new(logicalname: LogicalName, offset: u64, size: u64) -> Self { + Self { + logicalname, + offset, + size, + } + } +} +impl + Send + Sync> FileSystem for LoopDev { + async fn source(&self) -> Result>, Error> { + Ok(Some( + tokio::fs::canonicalize(self.logicalname.as_ref()).await?, + )) + } + fn mount_options(&self) -> impl IntoIterator { + [ + Box::new("loop") as Box, + Box::new(lazy_format!("offset={}", self.offset)), + Box::new(lazy_format!("sizelimit={}", self.size)), + ] + } + async fn source_hash( + &self, + ) -> Result::OutputSize>, Error> { + let mut sha = Sha256::new(); + sha.update("LoopDev"); + sha.update( + tokio::fs::canonicalize(self.logicalname.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.logicalname.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + sha.update(&u64::to_be_bytes(self.offset)[..]); + Ok(sha.finalize()) + } +} diff --git a/core/startos/src/disk/mount/filesystem/mod.rs b/core/startos/src/disk/mount/filesystem/mod.rs index 00247e0dd..80bfcc903 100644 --- a/core/startos/src/disk/mount/filesystem/mod.rs +++ b/core/startos/src/disk/mount/filesystem/mod.rs @@ -1,19 +1,27 @@ +use std::ffi::OsStr; +use std::fmt::{Display, Write}; use std::path::Path; -use async_trait::async_trait; use digest::generic_array::GenericArray; use digest::OutputSizeUser; +use futures::Future; use sha2::Sha256; +use tokio::process::Command; -use crate::Error; +use crate::prelude::*; +use crate::util::Invoke; +pub mod backupfs; pub mod bind; pub mod block_dev; pub mod cifs; pub mod ecryptfs; pub mod efivarfs; pub mod httpdirfs; +pub mod idmapped; pub mod label; +pub mod loop_dev; +pub mod overlayfs; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MountType { @@ -23,14 +31,79 @@ pub enum MountType { pub use MountType::*; -#[async_trait] -pub trait FileSystem { - async fn mount + Send + Sync>( +pub(self) async fn default_mount_command( + fs: &(impl FileSystem + ?Sized), + mountpoint: impl AsRef + Send, + mount_type: MountType, +) -> Result { + let mut cmd = std::process::Command::new("mount"); + if mount_type == ReadOnly { + cmd.arg("-r"); + } + cmd.args(fs.extra_args()); + if let Some(ty) = fs.mount_type() { + cmd.arg("-t").arg(ty.as_ref()); + } + if let Some(options) = fs + .mount_options() + .into_iter() + .fold(None, |acc: Option, x| match acc { + Some(mut s) => { + write!(s, ",{}", x).unwrap(); + Some(s) + } + None => Some(x.to_string()), + }) + { + cmd.arg("-o").arg(options); + } + if let Some(source) = fs.source().await? { + cmd.arg(source.as_ref()); + } + cmd.arg(mountpoint.as_ref()); + Ok(cmd) +} + +pub(self) async fn default_mount_impl( + fs: &(impl FileSystem + ?Sized), + mountpoint: impl AsRef + Send, + mount_type: MountType, +) -> Result<(), Error> { + fs.pre_mount().await?; + tokio::fs::create_dir_all(mountpoint.as_ref()).await?; + Command::from(default_mount_command(fs, mountpoint, mount_type).await?) + .capture(false) + .invoke(ErrorKind::Filesystem) + .await?; + + Ok(()) +} + +pub trait FileSystem: Send + Sync { + fn mount_type(&self) -> Option> { + None::<&str> + } + fn extra_args(&self) -> impl IntoIterator> { + [] as [&str; 0] + } + fn mount_options(&self) -> impl IntoIterator { + [] as [&str; 0] + } + fn source(&self) -> impl Future>, Error>> + Send { + async { Ok(None::<&Path>) } + } + fn pre_mount(&self) -> impl Future> + Send { + async { Ok(()) } + } + fn mount + Send>( &self, mountpoint: P, mount_type: MountType, - ) -> Result<(), Error>; - async fn source_hash( + ) -> impl Future> + Send { + default_mount_impl(self, mountpoint, mount_type) + } + fn source_hash( &self, - ) -> Result::OutputSize>, Error>; + ) -> impl Future::OutputSize>, Error>> + + Send; } diff --git a/core/startos/src/disk/mount/filesystem/overlayfs.rs b/core/startos/src/disk/mount/filesystem/overlayfs.rs new file mode 100644 index 000000000..e8d1f0b34 --- /dev/null +++ b/core/startos/src/disk/mount/filesystem/overlayfs.rs @@ -0,0 +1,164 @@ +use std::fmt::Display; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use digest::generic_array::GenericArray; +use digest::{Digest, OutputSizeUser}; +use sha2::Sha256; + +use crate::disk::mount::filesystem::{FileSystem, ReadWrite}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; +use crate::prelude::*; +use crate::util::io::TmpDir; + +pub struct OverlayFs, P1: AsRef, P2: AsRef> { + lower: P0, + upper: P1, + work: P2, +} +impl, P1: AsRef, P2: AsRef> OverlayFs { + pub fn new(lower: P0, upper: P1, work: P2) -> Self { + Self { lower, upper, work } + } +} +impl< + P0: AsRef + Send + Sync, + P1: AsRef + Send + Sync, + P2: AsRef + Send + Sync, + > FileSystem for OverlayFs +{ + fn mount_type(&self) -> Option> { + Some("overlay") + } + async fn source(&self) -> Result>, Error> { + Ok(Some("overlay")) + } + fn mount_options(&self) -> impl IntoIterator { + [ + Box::new(lazy_format!("lowerdir={}", self.lower.as_ref().display())) + as Box, + Box::new(lazy_format!("upperdir={}", self.upper.as_ref().display())), + Box::new(lazy_format!("workdir={}", self.work.as_ref().display())), + ] + } + async fn pre_mount(&self) -> Result<(), Error> { + tokio::fs::create_dir_all(self.upper.as_ref()).await?; + tokio::fs::create_dir_all(self.work.as_ref()).await?; + Ok(()) + } + async fn source_hash( + &self, + ) -> Result::OutputSize>, Error> { + tokio::fs::create_dir_all(self.upper.as_ref()).await?; + tokio::fs::create_dir_all(self.work.as_ref()).await?; + let mut sha = Sha256::new(); + sha.update("OverlayFs"); + sha.update( + tokio::fs::canonicalize(self.lower.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.lower.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + sha.update( + tokio::fs::canonicalize(self.upper.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.upper.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + sha.update( + tokio::fs::canonicalize(self.work.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.upper.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + Ok(sha.finalize()) + } +} + +#[derive(Debug)] +pub struct OverlayGuard { + lower: Option, + upper: Option, + inner_guard: MountGuard, +} +impl OverlayGuard { + pub async fn mount(lower: G, mountpoint: impl AsRef) -> Result { + let upper = TmpDir::new().await?; + let inner_guard = MountGuard::mount( + &OverlayFs::new( + lower.path(), + upper.as_ref().join("upper"), + upper.as_ref().join("work"), + ), + mountpoint, + ReadWrite, + ) + .await?; + Ok(Self { + lower: Some(lower), + upper: Some(upper), + inner_guard, + }) + } + pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> { + self.inner_guard.take().unmount(delete_mountpoint).await?; + if let Some(lower) = self.lower.take() { + lower.unmount().await?; + } + if let Some(upper) = self.upper.take() { + upper.delete().await?; + } + Ok(()) + } + pub fn take(&mut self) -> Self { + Self { + lower: self.lower.take(), + upper: self.upper.take(), + inner_guard: self.inner_guard.take(), + } + } +} +impl GenericMountGuard for OverlayGuard { + fn path(&self) -> &Path { + self.inner_guard.path() + } + async fn unmount(self) -> Result<(), Error> { + self.unmount(false).await + } +} +impl Drop for OverlayGuard { + fn drop(&mut self) { + let lower = self.lower.take(); + let upper = self.upper.take(); + let guard = self.inner_guard.take(); + if lower.is_some() || upper.is_some() || guard.mounted { + tokio::spawn(async move { + guard.unmount(false).await.log_err(); + if let Some(lower) = lower { + lower.unmount().await.log_err(); + } + if let Some(upper) = upper { + upper.delete().await.log_err(); + } + }); + } + } +} diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index 617afeb08..6e1cdc35e 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Weak}; +use futures::Future; use lazy_static::lazy_static; use models::ResultExt; use tokio::sync::Mutex; @@ -9,20 +10,44 @@ use tracing::instrument; use super::filesystem::{FileSystem, MountType, ReadOnly, ReadWrite}; use super::util::unmount; -use crate::util::Invoke; +use crate::util::{Invoke, Never}; use crate::Error; -pub const TMP_MOUNTPOINT: &'static str = "/media/embassy/tmp"; +pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp"; -#[async_trait::async_trait] -pub trait GenericMountGuard: AsRef + std::fmt::Debug + Send + Sync + 'static { - async fn unmount(mut self) -> Result<(), Error>; +pub trait GenericMountGuard: std::fmt::Debug + Send + Sync + 'static { + fn path(&self) -> &Path; + fn unmount(self) -> impl Future> + Send; +} + +impl GenericMountGuard for Never { + fn path(&self) -> &Path { + match *self {} + } + async fn unmount(self) -> Result<(), Error> { + match self {} + } +} + +impl GenericMountGuard for Arc +where + T: GenericMountGuard, +{ + fn path(&self) -> &Path { + (&**self).path() + } + async fn unmount(self) -> Result<(), Error> { + if let Ok(guard) = Arc::try_unwrap(self) { + guard.unmount().await?; + } + Ok(()) + } } #[derive(Debug)] pub struct MountGuard { mountpoint: PathBuf, - mounted: bool, + pub(super) mounted: bool, } impl MountGuard { pub async fn mount( @@ -37,9 +62,19 @@ impl MountGuard { mounted: true, }) } + fn as_unmounted(&self) -> Self { + Self { + mountpoint: self.mountpoint.clone(), + mounted: false, + } + } + pub fn take(&mut self) -> Self { + let unmounted = self.as_unmounted(); + std::mem::replace(self, unmounted) + } pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> { if self.mounted { - unmount(&self.mountpoint).await?; + unmount(&self.mountpoint, false).await?; if delete_mountpoint { match tokio::fs::remove_dir(&self.mountpoint).await { Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty @@ -57,30 +92,27 @@ impl MountGuard { Ok(()) } } -impl AsRef for MountGuard { - fn as_ref(&self) -> &Path { - &self.mountpoint - } -} impl Drop for MountGuard { fn drop(&mut self) { if self.mounted { let mountpoint = std::mem::take(&mut self.mountpoint); - tokio::spawn(async move { unmount(mountpoint).await.unwrap() }); + tokio::spawn(async move { unmount(mountpoint, true).await.log_err() }); } } } -#[async_trait::async_trait] impl GenericMountGuard for MountGuard { - async fn unmount(mut self) -> Result<(), Error> { + fn path(&self) -> &Path { + &self.mountpoint + } + async fn unmount(self) -> Result<(), Error> { MountGuard::unmount(self, false).await } } async fn tmp_mountpoint(source: &impl FileSystem) -> Result { Ok(Path::new(TMP_MOUNTPOINT).join(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - &source.source_hash().await?, + base32::Alphabet::Rfc4648 { padding: false }, + &source.source_hash().await?[0..20], ))) } @@ -89,7 +121,7 @@ lazy_static! { Mutex::new(BTreeMap::new()); } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct TmpMountGuard { guard: Arc, } @@ -122,21 +154,40 @@ impl TmpMountGuard { Ok(TmpMountGuard { guard }) } } - pub async fn unmount(self) -> Result<(), Error> { - if let Ok(guard) = Arc::try_unwrap(self.guard) { - guard.unmount(true).await?; - } - Ok(()) + + pub fn take(&mut self) -> Self { + let unmounted = Self { + guard: Arc::new(self.guard.as_unmounted()), + }; + std::mem::replace(self, unmounted) } } -impl AsRef for TmpMountGuard { - fn as_ref(&self) -> &Path { - (&*self.guard).as_ref() +impl GenericMountGuard for TmpMountGuard { + fn path(&self) -> &Path { + self.guard.path() + } + async fn unmount(self) -> Result<(), Error> { + self.guard.unmount().await } } -#[async_trait::async_trait] -impl GenericMountGuard for TmpMountGuard { - async fn unmount(mut self) -> Result<(), Error> { - TmpMountGuard::unmount(self).await + +#[derive(Debug)] +pub struct SubPath { + guard: G, + path: PathBuf, +} +impl SubPath { + pub fn new(guard: G, path: impl AsRef) -> Self { + let path = path.as_ref(); + let path = guard.path().join(path.strip_prefix("/").unwrap_or(path)); + Self { guard, path } + } +} +impl GenericMountGuard for SubPath { + fn path(&self) -> &Path { + self.path.as_path() + } + async fn unmount(self) -> Result<(), Error> { + self.guard.unmount().await } } diff --git a/core/startos/src/disk/mount/util.rs b/core/startos/src/disk/mount/util.rs index 392e5d67a..61368e67a 100644 --- a/core/startos/src/disk/mount/util.rs +++ b/core/startos/src/disk/mount/util.rs @@ -5,6 +5,16 @@ use tracing::instrument; use crate::util::Invoke; use crate::Error; +pub async fn is_mountpoint(path: impl AsRef) -> Result { + let is_mountpoint = tokio::process::Command::new("mountpoint") + .arg(path.as_ref()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await?; + Ok(is_mountpoint.success()) +} + #[instrument(skip_all)] pub async fn bind, P1: AsRef>( src: P0, @@ -16,14 +26,8 @@ pub async fn bind, P1: AsRef>( src.as_ref().display(), dst.as_ref().display() ); - let is_mountpoint = tokio::process::Command::new("mountpoint") - .arg(dst.as_ref()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .await?; - if is_mountpoint.success() { - unmount(dst.as_ref()).await?; + if is_mountpoint(&dst).await? { + unmount(dst.as_ref(), true).await?; } tokio::fs::create_dir_all(&src).await?; tokio::fs::create_dir_all(&dst).await?; @@ -41,11 +45,14 @@ pub async fn bind, P1: AsRef>( } #[instrument(skip_all)] -pub async fn unmount>(mountpoint: P) -> Result<(), Error> { +pub async fn unmount>(mountpoint: P, lazy: bool) -> Result<(), Error> { tracing::debug!("Unmounting {}.", mountpoint.as_ref().display()); - tokio::process::Command::new("umount") - .arg("-l") - .arg(mountpoint.as_ref()) + let mut cmd = tokio::process::Command::new("umount"); + cmd.arg("-R"); + if lazy { + cmd.arg("-l"); + } + cmd.arg(mountpoint.as_ref()) .invoke(crate::ErrorKind::Filesystem) .await?; Ok(()) diff --git a/core/startos/src/disk/util.rs b/core/startos/src/disk/util.rs index 7051026cd..c64ea40ae 100644 --- a/core/startos/src/disk/util.rs +++ b/core/startos/src/disk/util.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; +use chrono::{DateTime, Utc}; use color_eyre::eyre::{self, eyre}; use futures::TryStreamExt; use nom::bytes::complete::{tag, take_till1}; @@ -17,20 +18,22 @@ use tracing::instrument; use super::mount::filesystem::block_dev::BlockDev; use super::mount::filesystem::ReadOnly; use super::mount::guard::TmpMountGuard; +use crate::disk::mount::guard::GenericMountGuard; use crate::disk::OsPartitionInfo; +use crate::hostname::Hostname; use crate::util::serde::IoFormat; -use crate::util::{Invoke, Version}; +use crate::util::Invoke; use crate::{Error, ResultExt as _}; #[derive(Clone, Copy, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub enum PartitionTable { Mbr, Gpt, } #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct DiskInfo { pub logicalname: PathBuf, pub partition_table: Option, @@ -42,21 +45,22 @@ pub struct DiskInfo { } #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct PartitionInfo { pub logicalname: PathBuf, pub label: Option, pub capacity: u64, pub used: Option, - pub embassy_os: Option, + pub start_os: BTreeMap, pub guid: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct EmbassyOsRecoveryInfo { - pub version: Version, - pub full: bool, +#[serde(rename_all = "camelCase")] +pub struct StartOsRecoveryInfo { + pub hostname: Hostname, + pub version: exver::Version, + pub timestamp: DateTime, pub password_hash: Option, pub wrapped_key: Option, } @@ -222,29 +226,38 @@ pub async fn pvscan() -> Result>, Error> { pub async fn recovery_info( mountpoint: impl AsRef, -) -> Result, Error> { - let backup_unencrypted_metadata_path = mountpoint - .as_ref() - .join("EmbassyBackups/unencrypted-metadata.cbor"); - if tokio::fs::metadata(&backup_unencrypted_metadata_path) - .await - .is_ok() - { - return Ok(Some( - IoFormat::Cbor.from_slice( - &tokio::fs::read(&backup_unencrypted_metadata_path) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - backup_unencrypted_metadata_path.display().to_string(), - ) - })?, - )?, - )); +) -> Result, Error> { + let backup_root = mountpoint.as_ref().join("StartOSBackups"); + let mut res = BTreeMap::new(); + if tokio::fs::metadata(&backup_root).await.is_ok() { + let mut dir = tokio::fs::read_dir(&backup_root).await?; + while let Some(entry) = dir.next_entry().await? { + let server_id = entry.file_name().to_string_lossy().into_owned(); + let backup_unencrypted_metadata_path = backup_root + .join(&server_id) + .join("unencrypted-metadata.json"); + if tokio::fs::metadata(&backup_unencrypted_metadata_path) + .await + .is_ok() + { + res.insert( + server_id, + IoFormat::Json.from_slice( + &tokio::fs::read(&backup_unencrypted_metadata_path) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + backup_unencrypted_metadata_path.display().to_string(), + ) + })?, + )?, + ); + } + } } - Ok(None) + Ok(res) } #[instrument(skip_all)] @@ -389,7 +402,7 @@ async fn disk_info(disk: PathBuf) -> DiskInfo { } async fn part_info(part: PathBuf) -> PartitionInfo { - let mut embassy_os = None; + let mut start_os = BTreeMap::new(); let label = get_label(&part) .await .map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source)) @@ -403,20 +416,19 @@ async fn part_info(part: PathBuf) -> PartitionInfo { match TmpMountGuard::mount(&BlockDev::new(&part), ReadOnly).await { Err(e) => tracing::warn!("Could not collect usage information: {}", e.source), Ok(mount_guard) => { - used = get_used(&mount_guard) + used = get_used(mount_guard.path()) .await .map_err(|e| { tracing::warn!("Could not get usage of {}: {}", part.display(), e.source) }) .ok(); - if let Some(recovery_info) = match recovery_info(&mount_guard).await { - Ok(a) => a, + match recovery_info(mount_guard.path()).await { + Ok(a) => { + start_os = a; + } Err(e) => { tracing::error!("Error fetching unencrypted backup metadata: {}", e); - None } - } { - embassy_os = Some(recovery_info) } if let Err(e) = mount_guard.unmount().await { tracing::error!("Error unmounting partition {}: {}", part.display(), e); @@ -429,7 +441,7 @@ async fn part_info(part: PathBuf) -> PartitionInfo { label, capacity, used, - embassy_os, + start_os, guid: None, } } diff --git a/core/startos/src/error.rs b/core/startos/src/error.rs index 2b769b03a..a0ca5707c 100644 --- a/core/startos/src/error.rs +++ b/core/startos/src/error.rs @@ -1,4 +1,3 @@ -use color_eyre::eyre::eyre; pub use models::{Error, ErrorKind, OptionExt, ResultExt}; #[derive(Debug, Default)] @@ -18,11 +17,15 @@ impl ErrorCollection { } } - pub fn into_result(self) -> Result<(), Error> { - if self.0.is_empty() { - Ok(()) + pub fn into_result(mut self) -> Result<(), Error> { + if self.0.len() <= 1 { + if let Some(err) = self.0.pop() { + Err(err) + } else { + Ok(()) + } } else { - Err(Error::new(eyre!("{}", self), ErrorKind::MultipleErrors)) + Err(Error::new(self, ErrorKind::MultipleErrors)) } } } @@ -49,12 +52,13 @@ impl std::fmt::Display for ErrorCollection { Ok(()) } } +impl std::error::Error for ErrorCollection {} #[macro_export] macro_rules! ensure_code { ($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => { if !($x) { - return Err(crate::error::Error::new(color_eyre::eyre::eyre!($fmt, $($arg, )*), $c)); + Err::<(), _>(crate::error::Error::new(color_eyre::eyre::eyre!($fmt, $($arg, )*), $c))?; } }; } diff --git a/core/startos/src/firmware.rs b/core/startos/src/firmware.rs index 7f9a4a273..a70cf9e47 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -2,20 +2,18 @@ use std::collections::BTreeSet; use std::path::Path; use async_compression::tokio::bufread::GzipDecoder; -use clap::ArgMatches; -use rpc_toolkit::command; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use tokio::io::BufReader; use tokio::process::Command; use crate::disk::fsck::RequiresReboot; use crate::prelude::*; +use crate::util::io::open_file; use crate::util::Invoke; use crate::PLATFORM; /// Part of the Firmware, look there for more about -#[derive(Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct VersionMatcher { /// Strip this prefix on the version matcher @@ -29,7 +27,7 @@ pub struct VersionMatcher { /// Inside a file that is firmware.json, we /// wanted a structure that could help decide what to do /// for each of the firmware versions -#[derive(Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Firmware { id: String, @@ -43,20 +41,16 @@ pub struct Firmware { shasum: String, } -fn display_firmware_update_result(arg: RequiresReboot, _: &ArgMatches) { - if arg.0 { +pub fn display_firmware_update_result(result: RequiresReboot) { + if result.0 { println!("Firmware successfully updated! Reboot to apply changes."); } else { println!("No firmware update available."); } } -/// We wanted to make sure during every init -/// that the firmware was the correct and updated for -/// systems like the Pure System that a new firmware -/// was released and the updates where pushed through the pure os. -#[command(rename = "update-firmware", display(display_firmware_update_result))] -pub async fn update_firmware() -> Result { +#[instrument] +pub async fn check_for_firmware_update() -> Result, Error> { let system_product_name = String::from_utf8( Command::new("dmidecode") .arg("-s") @@ -76,22 +70,21 @@ pub async fn update_firmware() -> Result { .trim() .to_owned(); if system_product_name.is_empty() || bios_version.is_empty() { - return Ok(RequiresReboot(false)); + return Ok(None); } - let firmware_dir = Path::new("/usr/lib/startos/firmware"); - for firmware in serde_json::from_str::>( &tokio::fs::read_to_string("/usr/lib/startos/firmware.json").await?, ) .with_kind(ErrorKind::Deserialization)? { - let id = firmware.id; let matches_product_name = firmware .system_product_name - .map_or(true, |spn| spn == system_product_name); + .as_ref() + .map_or(true, |spn| spn == &system_product_name); let matches_bios_version = firmware .bios_version + .as_ref() .map_or(Some(true), |bv| { let mut semver_str = bios_version.as_str(); if let Some(prefix) = &bv.semver_prefix { @@ -115,35 +108,46 @@ pub async fn update_firmware() -> Result { }) .unwrap_or(false); if firmware.platform.contains(&*PLATFORM) && matches_product_name && matches_bios_version { - let filename = format!("{id}.rom.gz"); - let firmware_path = firmware_dir.join(&filename); - Command::new("sha256sum") - .arg("-c") - .input(Some(&mut std::io::Cursor::new(format!( - "{} {}", - firmware.shasum, - firmware_path.display() - )))) - .invoke(ErrorKind::Filesystem) - .await?; - let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { - GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?)) - } else { - return Err(Error::new( - eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), - ErrorKind::NotFound, - )); - }; - Command::new("flashrom") - .arg("-p") - .arg("internal") - .arg("-w-") - .input(Some(&mut rdr)) - .invoke(ErrorKind::Firmware) - .await?; - return Ok(RequiresReboot(true)); + return Ok(Some(firmware)); } } - Ok(RequiresReboot(false)) + Ok(None) +} + +/// We wanted to make sure during every init +/// that the firmware was the correct and updated for +/// systems like the Pure System that a new firmware +/// was released and the updates where pushed through the pure os. +#[instrument] +pub async fn update_firmware(firmware: Firmware) -> Result<(), Error> { + let id = &firmware.id; + let firmware_dir = Path::new("/usr/lib/startos/firmware"); + let filename = format!("{id}.rom.gz"); + let firmware_path = firmware_dir.join(&filename); + Command::new("sha256sum") + .arg("-c") + .input(Some(&mut std::io::Cursor::new(format!( + "{} {}", + firmware.shasum, + firmware_path.display() + )))) + .invoke(ErrorKind::Filesystem) + .await?; + let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { + GzipDecoder::new(BufReader::new(open_file(&firmware_path).await?)) + } else { + return Err(Error::new( + eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), + ErrorKind::NotFound, + )); + }; + Command::new("flashrom") + .arg("-p") + .arg("internal") + .arg("-w-") + .input(Some(&mut rdr)) + .invoke(ErrorKind::Firmware) + .await?; + Ok(()) } diff --git a/core/startos/src/hostname.rs b/core/startos/src/hostname.rs index f68d5c9d8..36bb5d8a4 100644 --- a/core/startos/src/hostname.rs +++ b/core/startos/src/hostname.rs @@ -1,11 +1,13 @@ +use imbl_value::InternedString; +use lazy_format::lazy_format; use rand::{thread_rng, Rng}; use tokio::process::Command; use tracing::instrument; use crate::util::Invoke; use crate::{Error, ErrorKind}; -#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)] -pub struct Hostname(pub String); +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Hostname(pub InternedString); lazy_static::lazy_static! { static ref ADJECTIVES: Vec = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); @@ -18,15 +20,16 @@ impl AsRef for Hostname { } impl Hostname { - pub fn lan_address(&self) -> String { - format!("https://{}.local", self.0) + pub fn lan_address(&self) -> InternedString { + InternedString::from_display(&lazy_format!("https://{}.local", self.0)) } - pub fn local_domain_name(&self) -> String { - format!("{}.local", self.0) + pub fn local_domain_name(&self) -> InternedString { + InternedString::from_display(&lazy_format!("{}.local", self.0)) } - pub fn no_dot_host_name(&self) -> String { - self.0.to_owned() + + pub fn no_dot_host_name(&self) -> InternedString { + self.0.clone() } } @@ -34,7 +37,9 @@ pub fn generate_hostname() -> Hostname { let mut rng = thread_rng(); let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())]; let noun = &NOUNS[rng.gen_range(0..NOUNS.len())]; - Hostname(format!("{adjective}-{noun}")) + Hostname(InternedString::from_display(&lazy_format!( + "{adjective}-{noun}" + ))) } pub fn generate_id() -> String { @@ -48,12 +53,12 @@ pub async fn get_current_hostname() -> Result { .invoke(ErrorKind::ParseSysInfo) .await?; let out_string = String::from_utf8(out)?; - Ok(Hostname(out_string.trim().to_owned())) + Ok(Hostname(out_string.trim().into())) } #[instrument(skip_all)] pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> { - let hostname: &String = &hostname.0; + let hostname = &*hostname.0; Command::new("hostnamectl") .arg("--static") .arg("set-hostname") diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 74c3767e3..63642dfb3 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -1,33 +1,51 @@ use std::fs::Permissions; +use std::io::Cursor; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::os::unix::fs::PermissionsExt; use std::path::Path; +use std::sync::Arc; use std::time::{Duration, SystemTime}; +use axum::extract::ws::{self}; use color_eyre::eyre::eyre; - +use const_format::formatcp; +use futures::{StreamExt, TryStreamExt}; +use itertools::Itertools; use models::ResultExt; use rand::random; -use sqlx::{Pool, Postgres}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::instrument; +use ts_rs::TS; use crate::account::AccountInfo; -use crate::context::rpc::RpcContextConfig; -use crate::db::model::ServerStatus; +use crate::context::config::ServerConfig; +use crate::context::{CliContext, InitContext}; +use crate::db::model::public::ServerStatus; +use crate::db::model::Database; use crate::disk::mount::util::unmount; -use crate::install::PKG_ARCHIVE_DIR; +use crate::hostname::Hostname; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; +use crate::net::net_controller::{NetController, NetService}; +use crate::net::utils::find_wifi_iface; +use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter}; use crate::prelude::*; - -use crate::util::cpupower::{ - get_available_governors, get_preferred_governor, set_governor, +use crate::progress::{ + FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, }; -use crate::util::docker::{create_bridge_network, CONTAINER_DATADIR, CONTAINER_TOOL}; -use crate::util::Invoke; -use crate::{Error, ARCH}; - -pub const SYSTEM_REBUILD_PATH: &str = "/media/embassy/config/system-rebuild"; -pub const STANDBY_MODE_PATH: &str = "/media/embassy/config/standby"; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; +use crate::ssh::SSH_DIR; +use crate::system::get_mem_info; +use crate::util::io::{create_file, IOHook}; +use crate::util::lshw::lshw; +use crate::util::net::WebSocketExt; +use crate::util::{cpupower, Invoke}; +use crate::{Error, MAIN_DATA, PACKAGE_DATA}; + +pub const SYSTEM_REBUILD_PATH: &str = "/media/startos/config/system-rebuild"; +pub const STANDBY_MODE_PATH: &str = "/media/startos/config/standby"; pub async fn check_time_is_synchronized() -> Result { Ok(String::from_utf8( @@ -54,7 +72,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { .await? .success() { - unmount("/var/lib/postgresql").await?; + unmount("/var/lib/postgresql", true).await?; } let exists = tokio::fs::metadata(&db_dir).await.is_ok(); if !exists { @@ -128,10 +146,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { old_version -= 1; let old_datadir = db_dir.join(old_version.to_string()); if tokio::fs::metadata(&old_datadir).await.is_ok() { - tokio::fs::File::create(&incomplete_path) - .await? - .sync_all() - .await?; + create_file(&incomplete_path).await?.sync_all().await?; Command::new("pg_upgradecluster") .arg(old_version.to_string()) .arg("main") @@ -185,15 +200,113 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { } pub struct InitResult { - pub secret_store: Pool, - pub db: patch_db::PatchDb, + pub net_ctrl: Arc, + pub os_net_service: NetService, +} + +pub struct InitPhases { + preinit: Option, + local_auth: PhaseProgressTrackerHandle, + load_database: PhaseProgressTrackerHandle, + load_ssh_keys: PhaseProgressTrackerHandle, + start_net: PhaseProgressTrackerHandle, + mount_logs: PhaseProgressTrackerHandle, + load_ca_cert: PhaseProgressTrackerHandle, + load_wifi: PhaseProgressTrackerHandle, + init_tmp: PhaseProgressTrackerHandle, + set_governor: PhaseProgressTrackerHandle, + sync_clock: PhaseProgressTrackerHandle, + enable_zram: PhaseProgressTrackerHandle, + update_server_info: PhaseProgressTrackerHandle, + launch_service_network: PhaseProgressTrackerHandle, + validate_db: PhaseProgressTrackerHandle, + postinit: Option, +} +impl InitPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + preinit: if Path::new("/media/startos/config/preinit.sh").exists() { + Some(handle.add_phase("Running preinit.sh".into(), Some(5))) + } else { + None + }, + local_auth: handle.add_phase("Enabling local authentication".into(), Some(1)), + load_database: handle.add_phase("Loading database".into(), Some(5)), + load_ssh_keys: handle.add_phase("Loading SSH Keys".into(), Some(1)), + start_net: handle.add_phase("Starting network controller".into(), Some(1)), + mount_logs: handle.add_phase("Switching logs to write to data drive".into(), Some(1)), + load_ca_cert: handle.add_phase("Loading CA certificate".into(), Some(1)), + load_wifi: handle.add_phase("Loading WiFi configuration".into(), Some(1)), + init_tmp: handle.add_phase("Initializing temporary files".into(), Some(1)), + set_governor: handle.add_phase("Setting CPU performance profile".into(), Some(1)), + sync_clock: handle.add_phase("Synchronizing system clock".into(), Some(10)), + enable_zram: handle.add_phase("Enabling ZRAM".into(), Some(1)), + update_server_info: handle.add_phase("Updating server info".into(), Some(1)), + launch_service_network: handle.add_phase("Launching service intranet".into(), Some(1)), + validate_db: handle.add_phase("Validating database".into(), Some(1)), + postinit: if Path::new("/media/startos/config/postinit.sh").exists() { + Some(handle.add_phase("Running postinit.sh".into(), Some(5))) + } else { + None + }, + } + } +} + +pub async fn run_script>(path: P, mut progress: PhaseProgressTrackerHandle) { + let script = path.as_ref(); + progress.start(); + if let Err(e) = async { + let script = tokio::fs::read_to_string(script).await?; + progress.set_total(script.as_bytes().iter().filter(|b| **b == b'\n').count() as u64); + let mut reader = IOHook::new(Cursor::new(script.as_bytes())); + reader.post_read(|buf| progress += buf.iter().filter(|b| **b == b'\n').count() as u64); + Command::new("/bin/bash") + .input(Some(&mut reader)) + .invoke(ErrorKind::Unknown) + .await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error Running {}: {}", script.display(), e); + tracing::debug!("{:?}", e); + } + progress.complete(); } #[instrument(skip_all)] -pub async fn init(cfg: &RpcContextConfig) -> Result { - tokio::fs::create_dir_all("/run/embassy") +pub async fn init( + webserver: &WebServerAcceptorSetter, + cfg: &ServerConfig, + InitPhases { + preinit, + mut local_auth, + mut load_database, + mut load_ssh_keys, + mut start_net, + mut mount_logs, + mut load_ca_cert, + mut load_wifi, + mut init_tmp, + mut set_governor, + mut sync_clock, + mut enable_zram, + mut update_server_info, + mut launch_service_network, + mut validate_db, + postinit, + }: InitPhases, +) -> Result { + if let Some(progress) = preinit { + run_script("/media/startos/config/preinit.sh", progress).await; + } + + local_auth.start(); + tokio::fs::create_dir_all("/run/startos") .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/embassy"))?; + .with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/startos"))?; if tokio::fs::metadata(LOCAL_AUTH_COOKIE_PATH).await.is_err() { tokio::fs::write( LOCAL_AUTH_COOKIE_PATH, @@ -208,49 +321,54 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { })?; tokio::fs::set_permissions(LOCAL_AUTH_COOKIE_PATH, Permissions::from_mode(0o046)).await?; Command::new("chown") - .arg("root:embassy") + .arg("root:startos") .arg(LOCAL_AUTH_COOKIE_PATH) .invoke(crate::ErrorKind::Filesystem) .await?; } + local_auth.complete(); - let secret_store = cfg.secret_store().await?; - tracing::info!("Opened Postgres"); - - crate::ssh::sync_keys_from_db(&secret_store, "/home/start9/.ssh/authorized_keys").await?; - tracing::info!("Synced SSH Keys"); - - let account = AccountInfo::load(&secret_store).await?; - let db = cfg.db(&account).await?; - tracing::info!("Opened PatchDB"); + load_database.start(); + let db = cfg.db().await?; + crate::version::Current::default().pre_init(&db).await?; + let db = TypedPatchDb::::load_unchecked(db); let peek = db.peek().await; - let mut server_info = peek.as_server_info().de()?; + load_database.complete(); + tracing::info!("Opened PatchDB"); - // write to ca cert store - tokio::fs::write( - "/usr/local/share/ca-certificates/startos-root-ca.crt", - account.root_ca_cert.to_pem()?, + load_ssh_keys.start(); + crate::ssh::sync_keys( + &Hostname(peek.as_public().as_server_info().as_hostname().de()?), + &peek.as_private().as_ssh_privkey().de()?, + &peek.as_private().as_ssh_pubkeys().de()?, + SSH_DIR, ) .await?; - Command::new("update-ca-certificates") - .invoke(crate::ErrorKind::OpenSsl) - .await?; + load_ssh_keys.complete(); + tracing::info!("Synced SSH Keys"); - if let Some(wifi_interface) = &cfg.wifi_interface { - crate::net::wifi::synchronize_wpa_supplicant_conf( - &cfg.datadir().join("main"), - wifi_interface, - &server_info.last_wifi_region, + let account = AccountInfo::load(&peek)?; + + start_net.start(); + let net_ctrl = Arc::new( + NetController::init( + db.clone(), + cfg.tor_control + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), + cfg.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + 9050, + ))), + &account.hostname, ) - .await?; - tracing::info!("Synchronized WiFi"); - } - - let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok() - || &*server_info.version < &emver::Version::new(0, 3, 2, 0) - || (*ARCH == "x86_64" && &*server_info.version < &emver::Version::new(0, 3, 4, 0)); - - let log_dir = cfg.datadir().join("main/logs"); + .await?, + ); + webserver.try_upgrade(|a| net_ctrl.net_iface.upgrade_listener(a))?; + let os_net_service = net_ctrl.os_bindings().await?; + start_net.complete(); + + mount_logs.start(); + let log_dir = Path::new(MAIN_DATA).join("logs"); if tokio::fs::metadata(&log_dir).await.is_err() { tokio::fs::create_dir_all(&log_dir).await?; } @@ -278,113 +396,84 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { .arg("systemd-journald") .invoke(crate::ErrorKind::Journald) .await?; + mount_logs.complete(); tracing::info!("Mounted Logs"); - let tmp_dir = cfg.datadir().join("package-data/tmp"); - if should_rebuild && tokio::fs::metadata(&tmp_dir).await.is_ok() { + load_ca_cert.start(); + // write to ca cert store + tokio::fs::write( + "/usr/local/share/ca-certificates/startos-root-ca.crt", + account.root_ca_cert.to_pem()?, + ) + .await?; + Command::new("update-ca-certificates") + .invoke(crate::ErrorKind::OpenSsl) + .await?; + load_ca_cert.complete(); + + load_wifi.start(); + let wifi_interface = find_wifi_iface().await?; + let wifi = db + .mutate(|db| { + let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + wifi.as_interface_mut().ser(&wifi_interface)?; + wifi.de() + }) + .await?; + crate::net::wifi::synchronize_network_manager(MAIN_DATA, &wifi).await?; + load_wifi.complete(); + tracing::info!("Synchronized WiFi"); + + init_tmp.start(); + let tmp_dir = Path::new(PACKAGE_DATA).join("tmp"); + if tokio::fs::metadata(&tmp_dir).await.is_ok() { tokio::fs::remove_dir_all(&tmp_dir).await?; } if tokio::fs::metadata(&tmp_dir).await.is_err() { tokio::fs::create_dir_all(&tmp_dir).await?; } - let tmp_var = cfg.datadir().join(format!("package-data/tmp/var")); + let tmp_var = Path::new(PACKAGE_DATA).join("tmp/var"); if tokio::fs::metadata(&tmp_var).await.is_ok() { tokio::fs::remove_dir_all(&tmp_var).await?; } crate::disk::mount::util::bind(&tmp_var, "/var/tmp", false).await?; - let tmp_docker = cfg - .datadir() - .join(format!("package-data/tmp/{CONTAINER_TOOL}")); - let tmp_docker_exists = tokio::fs::metadata(&tmp_docker).await.is_ok(); - if CONTAINER_TOOL == "docker" { - Command::new("systemctl") - .arg("stop") - .arg("docker") - .invoke(crate::ErrorKind::Docker) - .await?; + let downloading = Path::new(PACKAGE_DATA).join("archive/downloading"); + if tokio::fs::metadata(&downloading).await.is_ok() { + tokio::fs::remove_dir_all(&downloading).await?; } + let tmp_docker = Path::new(PACKAGE_DATA).join(formatcp!("tmp/{CONTAINER_TOOL}")); crate::disk::mount::util::bind(&tmp_docker, CONTAINER_DATADIR, false).await?; - - if CONTAINER_TOOL == "docker" { - Command::new("systemctl") - .arg("reset-failed") - .arg("docker") - .invoke(crate::ErrorKind::Docker) - .await?; - Command::new("systemctl") - .arg("start") - .arg("docker") - .invoke(crate::ErrorKind::Docker) - .await?; - } - tracing::info!("Mounted Docker Data"); - - if should_rebuild || !tmp_docker_exists { - if CONTAINER_TOOL == "docker" { - tracing::info!("Creating Docker Network"); - create_bridge_network("start9", "172.18.0.1/24", "br-start9").await?; - tracing::info!("Created Docker Network"); - } - - let datadir = cfg.datadir(); - tracing::info!("Loading System Docker Images"); - crate::install::rebuild_from("/usr/lib/startos/system-images", &datadir).await?; - tracing::info!("Loaded System Docker Images"); - - tracing::info!("Loading Package Docker Images"); - crate::install::rebuild_from(datadir.join(PKG_ARCHIVE_DIR), &datadir).await?; - tracing::info!("Loaded Package Docker Images"); - } - - if CONTAINER_TOOL == "podman" { - crate::util::docker::remove_container("netdummy", true).await?; - Command::new("podman") - .arg("run") - .arg("-d") - .arg("--rm") - .arg("--init") - .arg("--network=start9") - .arg("--name=netdummy") - .arg("start9/x_system/utils:latest") - .arg("sleep") - .arg("infinity") - .invoke(crate::ErrorKind::Docker) - .await?; - } - - tracing::info!("Enabling Docker QEMU Emulation"); - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--privileged") - .arg("--rm") - .arg("start9/x_system/binfmt") - .arg("--install") - .arg("all") - .invoke(crate::ErrorKind::Docker) - .await?; - tracing::info!("Enabled Docker QEMU Emulation"); - - let governor = if let Some(governor) = &server_info.governor { - if get_available_governors().await?.contains(governor) { + init_tmp.complete(); + + let server_info = db.peek().await.into_public().into_server_info(); + set_governor.start(); + let selected_governor = server_info.as_governor().de()?; + let governor = if let Some(governor) = &selected_governor { + if cpupower::get_available_governors() + .await? + .contains(governor) + { Some(governor) } else { tracing::warn!("CPU Governor \"{governor}\" Not Available"); None } } else { - get_preferred_governor().await? + cpupower::get_preferred_governor().await? }; if let Some(governor) = governor { tracing::info!("Setting CPU Governor to \"{governor}\""); - set_governor(governor).await?; + cpupower::set_governor(governor).await?; tracing::info!("Set CPU Governor"); } + set_governor.complete(); - server_info.ntp_synced = false; + sync_clock.start(); + let mut ntp_synced = false; let mut not_made_progress = 0u32; for _ in 0..1800 { if check_time_is_synchronized().await? { - server_info.ntp_synced = true; + ntp_synced = true; break; } let t = SystemTime::now(); @@ -401,47 +490,197 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { break; } } - if !server_info.ntp_synced { + if !ntp_synced { tracing::warn!("Timed out waiting for system time to synchronize"); } else { tracing::info!("Syncronized system clock"); } + sync_clock.complete(); - if server_info.zram { - crate::system::enable_zram().await? + enable_zram.start(); + if server_info.as_zram().de()? { + crate::system::enable_zram().await?; + tracing::info!("Enabled ZRAM"); } - server_info.ip_info = crate::net::dhcp::init_ips().await?; - server_info.status_info = ServerStatus { + enable_zram.complete(); + + update_server_info.start(); + let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024; + let devices = lshw().await?; + let status_info = ServerStatus { updated: false, update_progress: None, backup_progress: None, shutting_down: false, restarting: false, }; - db.mutate(|v| { - v.as_server_info_mut().ser(&server_info)?; + let server_info = v.as_public_mut().as_server_info_mut(); + server_info.as_ntp_synced_mut().ser(&ntp_synced)?; + server_info.as_ram_mut().ser(&ram)?; + server_info.as_devices_mut().ser(&devices)?; + server_info.as_status_info_mut().ser(&status_info)?; Ok(()) }) .await?; + tracing::info!("Updated server info"); + update_server_info.complete(); - crate::version::init(&db, &secret_store).await?; + launch_service_network.start(); + Command::new("systemctl") + .arg("start") + .arg("lxc-net.service") + .invoke(ErrorKind::Lxc) + .await?; + tracing::info!("Launched service intranet"); + launch_service_network.complete(); + validate_db.start(); db.mutate(|d| { let model = d.de()?; d.ser(&model) }) .await?; + tracing::info!("Validated database"); + validate_db.complete(); - if should_rebuild { - match tokio::fs::remove_file(SYSTEM_REBUILD_PATH).await { - Ok(()) => Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - }?; + if let Some(progress) = postinit { + run_script("/media/startos/config/postinit.sh", progress).await; } tracing::info!("System initialized."); - Ok(InitResult { secret_store, db }) + Ok(InitResult { + net_ctrl, + os_net_service, + }) +} + +pub fn init_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "logs", + crate::system::logs::().with_about("Disply OS logs"), + ) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::) + .no_display() + .with_about("Display OS logs"), + ) + .subcommand( + "kernel-logs", + crate::system::kernel_logs::().with_about("Display kernel logs"), + ) + .subcommand( + "kernel-logs", + from_fn_async(crate::logs::cli_logs::) + .no_display() + .with_about("Display kernel logs"), + ) + .subcommand("subscribe", from_fn_async(init_progress).no_cli()) + .subcommand( + "subscribe", + from_fn_async(cli_init_progress) + .no_display() + .with_about("Get initialization progress"), + ) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct InitProgressRes { + pub progress: FullProgress, + pub guid: Guid, +} + +pub async fn init_progress(ctx: InitContext) -> Result { + let progress_tracker = ctx.progress.clone(); + let progress = progress_tracker.snapshot(); + let mut error = ctx.error.subscribe(); + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + let res = tokio::try_join!( + async { + let mut stream = + progress_tracker.stream(Some(Duration::from_millis(100))); + while let Some(progress) = stream.next().await { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + break; + } + } + + Ok::<_, Error>(()) + }, + async { + if let Some(e) = error + .wait_for(|e| e.is_some()) + .await + .ok() + .and_then(|e| e.as_ref().map(|e| e.clone_output())) + { + Err::<(), _>(e) + } else { + Ok(()) + } + } + ); + + if let Err(e) = ws + .close_result(res.map(|_| "complete").map_err(|e| { + tracing::error!("error in init progress websocket: {e}"); + tracing::debug!("{e:?}"); + e + })) + .await + { + tracing::error!("error closing init progress websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + Ok(InitProgressRes { progress, guid }) +} + +pub async fn cli_init_progress( + HandlerArgs { + context: ctx, + parent_method, + method, + raw_params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let res: InitProgressRes = from_value( + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method.into_iter()) + .join("."), + raw_params, + ) + .await?, + )?; + let mut ws = ctx.ws_continuation(res.guid).await?; + let mut bar = PhasedProgressBar::new("Initializing..."); + while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? { + if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg { + bar.update(&serde_json::from_str(&msg).with_kind(ErrorKind::Deserialization)?); + } + } + Ok(()) } diff --git a/core/startos/src/inspect.rs b/core/startos/src/inspect.rs deleted file mode 100644 index cd27bbb2d..000000000 --- a/core/startos/src/inspect.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::path::PathBuf; - -use rpc_toolkit::command; - -use crate::s9pk::manifest::Manifest; -use crate::s9pk::reader::S9pkReader; -use crate::util::display_none; -use crate::util::serde::{display_serializable, IoFormat}; -use crate::Error; - -#[command(subcommands(hash, manifest, license, icon, instructions, docker_images))] -pub fn inspect() -> Result<(), Error> { - Ok(()) -} - -#[command(cli_only)] -pub async fn hash(#[arg] path: PathBuf) -> Result { - Ok(S9pkReader::open(path, true) - .await? - .hash_str() - .unwrap() - .to_owned()) -} - -#[command(cli_only, display(display_serializable))] -pub async fn manifest( - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { - S9pkReader::open(path, !no_verify).await?.manifest().await -} - -#[command(cli_only, display(display_none))] -pub async fn license( - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify).await?.license().await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} - -#[command(cli_only, display(display_none))] -pub async fn icon( - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify).await?.icon().await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} - -#[command(cli_only, display(display_none))] -pub async fn instructions( - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify) - .await? - .instructions() - .await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} - -#[command(cli_only, display(display_none), rename = "docker-images")] -pub async fn docker_images( - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify) - .await? - .docker_images() - .await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} diff --git a/core/startos/src/install/cleanup.rs b/core/startos/src/install/cleanup.rs deleted file mode 100644 index d90ec502c..000000000 --- a/core/startos/src/install/cleanup.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use models::OptionExt; -use sqlx::{Executor, Postgres}; -use tracing::instrument; - -use super::PKG_ARCHIVE_DIR; -use crate::context::RpcContext; -use crate::db::model::{ - CurrentDependencies, Database, PackageDataEntry, PackageDataEntryInstalled, - PackageDataEntryMatchModelRef, -}; -use crate::error::ErrorCollection; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::{Apply, Version}; -use crate::volume::{asset_dir, script_dir}; -use crate::Error; - -#[instrument(skip_all)] -pub async fn cleanup(ctx: &RpcContext, id: &PackageId, version: &Version) -> Result<(), Error> { - let mut errors = ErrorCollection::new(); - ctx.managers.remove(&(id.clone(), version.clone())).await; - // docker images start9/$APP_ID/*:$VERSION -q | xargs docker rmi - let images = crate::util::docker::images_for(id, version).await?; - errors.extend( - futures::future::join_all(images.into_iter().map(|sha| async { - let sha = sha; // move into future - crate::util::docker::remove_image(&sha).await - })) - .await, - ); - let pkg_archive_dir = ctx - .datadir - .join(PKG_ARCHIVE_DIR) - .join(id) - .join(version.as_str()); - if tokio::fs::metadata(&pkg_archive_dir).await.is_ok() { - tokio::fs::remove_dir_all(&pkg_archive_dir) - .await - .apply(|res| errors.handle(res)); - } - let assets_path = asset_dir(&ctx.datadir, id, version); - if tokio::fs::metadata(&assets_path).await.is_ok() { - tokio::fs::remove_dir_all(&assets_path) - .await - .apply(|res| errors.handle(res)); - } - let scripts_path = script_dir(&ctx.datadir, id, version); - if tokio::fs::metadata(&scripts_path).await.is_ok() { - tokio::fs::remove_dir_all(&scripts_path) - .await - .apply(|res| errors.handle(res)); - } - - errors.into_result() -} - -#[instrument(skip_all)] -pub async fn cleanup_failed(ctx: &RpcContext, id: &PackageId) -> Result<(), Error> { - if let Some(version) = match ctx - .db - .peek() - .await - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_match() - { - PackageDataEntryMatchModelRef::Installing(m) => Some(m.as_manifest().as_version().de()?), - PackageDataEntryMatchModelRef::Restoring(m) => Some(m.as_manifest().as_version().de()?), - PackageDataEntryMatchModelRef::Updating(m) => { - let manifest_version = m.as_manifest().as_version().de()?; - let installed = m.as_installed().as_manifest().as_version().de()?; - if manifest_version != installed { - Some(manifest_version) - } else { - None // do not remove existing data - } - } - _ => { - tracing::warn!("{}: Nothing to clean up!", id); - None - } - } { - cleanup(ctx, id, &version).await?; - } - - ctx.db - .mutate(|v| { - match v - .clone() - .into_package_data() - .into_idx(id) - .or_not_found(id)? - .as_match() - { - PackageDataEntryMatchModelRef::Installing(_) - | PackageDataEntryMatchModelRef::Restoring(_) => { - v.as_package_data_mut().remove(id)?; - } - PackageDataEntryMatchModelRef::Updating(pde) => { - v.as_package_data_mut() - .as_idx_mut(id) - .or_not_found(id)? - .ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { - manifest: pde.as_installed().as_manifest().de()?, - static_files: pde.as_static_files().de()?, - installed: pde.as_installed().de()?, - }))?; - } - _ => (), - } - Ok(()) - }) - .await -} - -#[instrument(skip_all)] -pub fn remove_from_current_dependents_lists( - db: &mut Model, - id: &PackageId, - current_dependencies: &CurrentDependencies, -) -> Result<(), Error> { - for dep in current_dependencies.0.keys().chain(std::iter::once(id)) { - if let Some(current_dependents) = db - .as_package_data_mut() - .as_idx_mut(dep) - .and_then(|d| d.as_installed_mut()) - .map(|i| i.as_current_dependents_mut()) - { - current_dependents.remove(id)?; - } - } - Ok(()) -} - -#[instrument(skip_all)] -pub async fn uninstall(ctx: &RpcContext, secrets: &mut Ex, id: &PackageId) -> Result<(), Error> -where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, -{ - let db = ctx.db.peek().await; - let entry = db - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .expect_as_removing()?; - - let dependents_paths: Vec = entry - .as_removing() - .as_current_dependents() - .keys()? - .into_iter() - .filter(|x| x != id) - .flat_map(|x| db.as_package_data().as_idx(&x)) - .flat_map(|x| x.as_installed()) - .flat_map(|x| x.as_manifest().as_volumes().de()) - .flat_map(|x| x.values().cloned().collect::>()) - .flat_map(|x| x.pointer_path(&ctx.datadir)) - .collect(); - - let volume_dir = ctx - .datadir - .join(crate::volume::PKG_VOLUME_DIR) - .join(&*entry.as_manifest().as_id().de()?); - let version = entry.as_removing().as_manifest().as_version().de()?; - tracing::debug!( - "Cleaning up {:?} except for {:?}", - volume_dir, - dependents_paths - ); - cleanup(ctx, id, &version).await?; - cleanup_folder(volume_dir, Arc::new(dependents_paths)).await; - remove_network_keys(secrets, id).await?; - - ctx.db - .mutate(|d| { - d.as_package_data_mut().remove(id)?; - remove_from_current_dependents_lists( - d, - id, - &entry.as_removing().as_current_dependencies().de()?, - ) - }) - .await -} - -#[instrument(skip_all)] -pub async fn remove_network_keys(secrets: &mut Ex, id: &PackageId) -> Result<(), Error> -where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, -{ - sqlx::query!("DELETE FROM network_keys WHERE package = $1", &*id) - .execute(&mut *secrets) - .await?; - sqlx::query!("DELETE FROM tor WHERE package = $1", &*id) - .execute(&mut *secrets) - .await?; - Ok(()) -} - -/// Needed to remove, without removing the folders that are mounted in the other docker containers -pub fn cleanup_folder( - path: PathBuf, - dependents_volumes: Arc>, -) -> futures::future::BoxFuture<'static, ()> { - Box::pin(async move { - let meta_data = match tokio::fs::metadata(&path).await { - Ok(a) => a, - Err(_e) => { - return; - } - }; - if !meta_data.is_dir() { - tracing::error!("is_not dir, remove {:?}", path); - let _ = tokio::fs::remove_file(&path).await; - return; - } - if !dependents_volumes - .iter() - .any(|v| v.starts_with(&path) || v == &path) - { - tracing::error!("No parents, remove {:?}", path); - let _ = tokio::fs::remove_dir_all(&path).await; - return; - } - let mut read_dir = match tokio::fs::read_dir(&path).await { - Ok(a) => a, - Err(_e) => { - return; - } - }; - tracing::error!("Parents, recurse {:?}", path); - while let Some(entry) = read_dir.next_entry().await.ok().flatten() { - let entry_path = entry.path(); - cleanup_folder(entry_path, dependents_volumes.clone()).await; - } - }) -} diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 01f405e7b..33c2ea6c7 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -1,99 +1,78 @@ -use std::collections::BTreeMap; -use std::io::SeekFrom; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; -use std::sync::atomic::Ordering; -use std::sync::Arc; +use std::ops::Deref; +use std::path::PathBuf; use std::time::Duration; +use axum::extract::ws; +use clap::builder::ValueParserFactory; +use clap::{value_parser, CommandFactory, FromArgMatches, Parser}; use color_eyre::eyre::eyre; -use emver::VersionRange; -use futures::future::BoxFuture; -use futures::{FutureExt, StreamExt, TryStreamExt}; -use http::header::CONTENT_LENGTH; -use http::{Request, Response, StatusCode}; -use hyper::Body; -use models::{mime, DataUrl}; +use exver::VersionRange; +use futures::{AsyncWriteExt, StreamExt}; +use imbl_value::{json, InternedString}; +use itertools::Itertools; +use models::{FromStrParser, VersionString}; +use reqwest::header::{HeaderMap, CONTENT_LENGTH}; use reqwest::Url; -use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; -use serde_json::{json, Value}; -use tokio::fs::{File, OpenOptions}; -use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWriteExt}; -use tokio::process::Command; +use rpc_toolkit::HandlerArgs; +use rustyline_async::ReadlineEvent; +use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; -use tokio_stream::wrappers::ReadDirStream; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use tracing::instrument; +use ts_rs::TS; -use self::cleanup::{cleanup_failed, remove_from_current_dependents_lists}; -use crate::config::ConfigureContext; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; -use crate::db::model::{ - CurrentDependencies, CurrentDependencyInfo, CurrentDependents, InstalledPackageInfo, - PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryInstalling, - PackageDataEntryMatchModelRef, PackageDataEntryRemoving, PackageDataEntryRestoring, - PackageDataEntryUpdating, StaticDependencyInfo, StaticFiles, -}; -use crate::dependencies::{ - add_dependent_to_current_dependents_lists, compute_dependency_config_errs, - set_dependents_with_live_pointers_to_needs_config, -}; -use crate::install::cleanup::cleanup; -use crate::install::progress::{InstallProgress, InstallProgressTracker}; -use crate::notifications::NotificationLevel; +use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; -use crate::registry::marketplace::with_query_params; -use crate::s9pk::manifest::{Manifest, PackageId}; -use crate::s9pk::reader::S9pkReader; -use crate::status::{MainStatus, Status}; -use crate::util::docker::CONTAINER_TOOL; -use crate::util::io::response_to_reader; -use crate::util::serde::{display_serializable, Port}; -use crate::util::{display_none, AsyncFileExt, Invoke, Version}; -use crate::volume::{asset_dir, script_dir}; -use crate::{Error, ErrorKind, ResultExt}; - -pub mod cleanup; -pub mod progress; +use crate::progress::{FullProgress, FullProgressTracker, PhasedProgressBar}; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; +use crate::registry::package::get::GetPackageResponse; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::s9pk::manifest::PackageId; +use crate::upload::upload; +use crate::util::io::open_file; +use crate::util::net::WebSocketExt; +use crate::util::Never; pub const PKG_ARCHIVE_DIR: &str = "package-data/archive"; pub const PKG_PUBLIC_DIR: &str = "package-data/public"; pub const PKG_WASM_DIR: &str = "package-data/wasm"; -#[command(display(display_serializable))] -pub async fn list(#[context] ctx: RpcContext) -> Result { - Ok(ctx.db.peek().await.as_package_data().as_entries()? +// #[command(display(display_serializable))] +pub async fn list(ctx: RpcContext) -> Result, Error> { + Ok(ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_entries()? .iter() .filter_map(|(id, pde)| { - let status = match pde.as_match() { - PackageDataEntryMatchModelRef::Installed(_) => { - "installed" - } - PackageDataEntryMatchModelRef::Installing(_) => { - "installing" - } - PackageDataEntryMatchModelRef::Updating(_) => { - "updating" - } - PackageDataEntryMatchModelRef::Restoring(_) => { - "restoring" - } - PackageDataEntryMatchModelRef::Removing(_) => { - "removing" - } - PackageDataEntryMatchModelRef::Error(_) => { - "error" - } + let status = match pde.as_state_info().as_match() { + PackageStateMatchModelRef::Installed(_) => "installed", + PackageStateMatchModelRef::Installing(_) => "installing", + PackageStateMatchModelRef::Updating(_) => "updating", + PackageStateMatchModelRef::Restoring(_) => "restoring", + PackageStateMatchModelRef::Removing(_) => "removing", + PackageStateMatchModelRef::Error(_) => "error", }; - serde_json::to_value(json!({ "status":status, "id": id.clone(), "version": pde.as_manifest().as_version().de().ok()?})) - .ok() + Some(json!({ + "status": status, + "id": id.clone(), + "version": pde.as_state_info() + .as_manifest(ManifestPreference::Old) + .as_version() + .de() + .ok()? + })) }) .collect()) } -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] pub enum MinMax { Min, Max, @@ -116,6 +95,12 @@ impl std::str::FromStr for MinMax { } } } +impl ValueParserFactory for MinMax { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} impl std::fmt::Display for MinMax { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -125,1193 +110,441 @@ impl std::fmt::Display for MinMax { } } -#[command( - custom_cli(cli_install(async, context(CliContext))), - display(display_none), - metadata(sync_db = true) -)] +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct InstallParams { + #[ts(type = "string")] + registry: Url, + id: PackageId, + version: VersionString, +} + #[instrument(skip_all)] pub async fn install( - #[context] ctx: RpcContext, - #[arg] id: String, - #[arg(short = 'm', long = "marketplace-url", rename = "marketplace-url")] - marketplace_url: Option, - #[arg(short = 'v', long = "version-spec", rename = "version-spec")] version_spec: Option< - String, - >, - #[arg(long = "version-priority", rename = "version-priority")] version_priority: Option, + ctx: RpcContext, + InstallParams { + registry, + id, + version, + }: InstallParams, ) -> Result<(), Error> { - let version_str = match &version_spec { - None => "*", - Some(v) => &*v, - }; - let version: VersionRange = version_str.parse()?; - let marketplace_url = - marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); - let version_priority = version_priority.unwrap_or_default(); - let man: Manifest = ctx - .client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/manifest/{}?spec={}&version-priority={}", - marketplace_url, id, version, version_priority, - ) - .parse()?, - )) - .send() - .await - .with_kind(crate::ErrorKind::Registry)? - .error_for_status() - .with_kind(crate::ErrorKind::Registry)? - .json() - .await - .with_kind(crate::ErrorKind::Registry)?; - let s9pk = ctx - .client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/{}.s9pk?spec=={}&version-priority={}", - marketplace_url, id, man.version, version_priority, - ) - .parse()?, - )) - .send() - .await - .with_kind(crate::ErrorKind::Registry)? - .error_for_status()?; - - if *man.id != *id || !man.version.satisfies(&version) { - return Err(Error::new( - eyre!("Fetched package does not match requested id and version"), - ErrorKind::Registry, - )); - } - - let public_dir_path = ctx - .datadir - .join(PKG_PUBLIC_DIR) - .join(&man.id) - .join(man.version.as_str()); - tokio::fs::create_dir_all(&public_dir_path).await?; - - let icon_type = man.assets.icon_type(); - let (license_res, instructions_res, icon_res) = tokio::join!( - async { - tokio::io::copy( - &mut response_to_reader( - ctx.client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/license/{}?spec=={}", - marketplace_url, id, man.version, - ) - .parse()?, - )) - .send() - .await? - .error_for_status()?, - ), - &mut File::create(public_dir_path.join("LICENSE.md")).await?, - ) - .await?; - Ok::<_, color_eyre::eyre::Report>(()) - }, - async { - tokio::io::copy( - &mut response_to_reader( - ctx.client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/instructions/{}?spec=={}", - marketplace_url, id, man.version, - ) - .parse()?, - )) - .send() - .await? - .error_for_status()?, - ), - &mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?, - ) - .await?; - Ok::<_, color_eyre::eyre::Report>(()) - }, - async { - tokio::io::copy( - &mut response_to_reader( - ctx.client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/icon/{}?spec=={}", - marketplace_url, id, man.version, - ) - .parse()?, - )) - .send() - .await? - .error_for_status()?, - ), - &mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?, + let package: GetPackageResponse = from_value( + ctx.call_remote_with::( + "package.get", + json!({ + "id": id, + "version": VersionRange::exactly(version.deref().clone()), + }), + RegistryUrlParams { + registry: registry.clone(), + }, + ) + .await?, + )?; + + let asset = &package + .best + .get(&version) + .ok_or_else(|| { + Error::new( + eyre!("{id}@{version} not found on {registry}"), + ErrorKind::NotFound, ) - .await?; - Ok::<_, color_eyre::eyre::Report>(()) - }, - ); - if let Err(e) = license_res { - tracing::warn!("Failed to pre-download license: {}", e); - } - if let Err(e) = instructions_res { - tracing::warn!("Failed to pre-download instructions: {}", e); - } - if let Err(e) = icon_res { - tracing::warn!("Failed to pre-download icon: {}", e); - } + })? + .s9pk; - let progress = Arc::new(InstallProgress::new(s9pk.content_length())); - let static_files = StaticFiles::local(&man.id, &man.version, icon_type); - ctx.db - .mutate(|db| { - let pde = match db - .as_package_data() - .as_idx(&man.id) - .map(|x| x.de()) - .transpose()? - { - Some(PackageDataEntry::Installed(PackageDataEntryInstalled { - installed, - static_files, - .. - })) => PackageDataEntry::Updating(PackageDataEntryUpdating { - install_progress: progress.clone(), - static_files, - installed, - manifest: man.clone(), - }), - None => PackageDataEntry::Installing(PackageDataEntryInstalling { - install_progress: progress.clone(), - static_files, - manifest: man.clone(), - }), - _ => { - return Err(Error::new( - eyre!("Cannot install over a package in a transient state"), - crate::ErrorKind::InvalidRequest, - )) - } - }; - db.as_package_data_mut().insert(&man.id, &pde) - }) + let download = ctx + .services + .install( + ctx.clone(), + || asset.deserialize_s9pk_buffered(ctx.client.clone()), + None::, + None, + ) .await?; - - let downloading = download_install_s9pk( - ctx.clone(), - man.clone(), - Some(marketplace_url), - Arc::new(InstallProgress::new(s9pk.content_length())), - response_to_reader(s9pk), - None, - ); - tokio::spawn(async move { - if let Err(e) = downloading.await { - let err_str = format!("Install of {}@{} Failed: {}", man.id, man.version, e); - tracing::error!("{}", err_str); - tracing::debug!("{:?}", e); - if let Err(e) = ctx - .notification_manager - .notify( - ctx.db.clone(), - Some(man.id), - NotificationLevel::Error, - String::from("Install Failed"), - err_str, - (), - None, - ) - .await - { - tracing::error!("Failed to issue Notification: {}", e); - tracing::debug!("{:?}", e); - } - } - Ok::<_, String>(()) - }); + tokio::spawn(async move { download.await?.await }); Ok(()) } -#[command(rpc_only, display(display_none))] -#[instrument(skip_all)] -pub async fn sideload( - #[context] ctx: RpcContext, - #[arg] manifest: Manifest, - #[arg] icon: Option, -) -> Result { - let new_ctx = ctx.clone(); - let guid = RequestGuid::new(); - if let Some(icon) = icon { - use tokio::io::AsyncWriteExt; - let public_dir_path = ctx - .datadir - .join(PKG_PUBLIC_DIR) - .join(&manifest.id) - .join(manifest.version.as_str()); - tokio::fs::create_dir_all(&public_dir_path).await?; - - let invalid_data_url = - || Error::new(eyre!("Invalid Icon Data URL"), ErrorKind::InvalidRequest); - let data = icon - .strip_prefix(&format!( - "data:image/{};base64,", - manifest.assets.icon_type() - )) - .ok_or_else(&invalid_data_url)?; - let mut icon_file = - File::create(public_dir_path.join(format!("icon.{}", manifest.assets.icon_type()))) - .await?; - icon_file - .write_all(&base64::decode(data).with_kind(ErrorKind::InvalidRequest)?) - .await?; - icon_file.sync_all().await?; - } +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SideloadParams { + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: Option, +} - let handler = Box::new(|req: Request| { - async move { - let content_length = match req.headers().get(CONTENT_LENGTH).map(|a| a.to_str()) { - None => None, - Some(Err(_)) => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Invalid Content Length")) - .with_kind(ErrorKind::Network) - } - Some(Ok(a)) => match a.parse::() { - Err(_) => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Invalid Content Length")) - .with_kind(ErrorKind::Network) - } - Ok(a) => Some(a), - }, - }; - let progress = Arc::new(InstallProgress::new(content_length)); - let install_progress = progress.clone(); +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SideloadResponse { + pub upload: Guid, + pub progress: Guid, +} - new_ctx - .db - .mutate(|db| { - let pde = match db - .as_package_data() - .as_idx(&manifest.id) - .map(|x| x.de()) - .transpose()? - { - Some(PackageDataEntry::Installed(PackageDataEntryInstalled { - installed, - static_files, - .. - })) => PackageDataEntry::Updating(PackageDataEntryUpdating { - install_progress, - installed, - manifest: manifest.clone(), - static_files, - }), - None => PackageDataEntry::Installing(PackageDataEntryInstalling { - install_progress, - static_files: StaticFiles::local( - &manifest.id, - &manifest.version, - &manifest.assets.icon_type(), - ), - manifest: manifest.clone(), - }), - _ => { - return Err(Error::new( - eyre!("Cannot install over a package in a transient state"), - crate::ErrorKind::InvalidRequest, - )) +#[instrument(skip_all)] +pub async fn sideload( + ctx: RpcContext, + SideloadParams { session }: SideloadParams, +) -> Result { + let (upload, file) = upload(&ctx, session.clone()).await?; + let (err_send, mut err_recv) = oneshot::channel::(); + let progress = Guid::new(); + let progress_tracker = FullProgressTracker::new(); + let mut progress_listener = progress_tracker.stream(Some(Duration::from_millis(200))); + ctx.rpc_continuations + .add( + progress.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + |mut ws| async move { + if let Err(e) = async { + loop { + tokio::select! { + progress = progress_listener.next() => { + if let Some(progress) = progress { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + return ws.normal_close("complete").await; + } + } else { + return ws.normal_close("complete").await; + } + } + msg = ws.recv() => { + if msg.transpose().with_kind(ErrorKind::Network)?.is_none() { + return Ok(()) + } + } + err = (&mut err_recv) => { + if let Ok(e) = err { + ws.close_result(Err::<&str, _>(e.clone_output())).await?; + return Err(e) + } + } + } } - }; - db.as_package_data_mut().insert(&manifest.id, &pde) - }) - .await?; - - let (send, recv) = oneshot::channel(); - - tokio::spawn(async move { - if let Err(e) = download_install_s9pk( - new_ctx.clone(), - manifest.clone(), - None, - progress, - tokio_util::io::StreamReader::new(req.into_body().map_err(|e| { - std::io::Error::new( - match &e { - e if e.is_connect() => std::io::ErrorKind::ConnectionRefused, - e if e.is_timeout() => std::io::ErrorKind::TimedOut, - _ => std::io::ErrorKind::Other, - }, - e, - ) - })), - Some(send), - ) - .await - { - let err_str = format!( - "Install of {}@{} Failed: {}", - manifest.id, manifest.version, e - ); - tracing::error!("{}", err_str); - tracing::debug!("{:?}", e); - if let Err(e) = new_ctx - .notification_manager - .notify( - new_ctx.db.clone(), - Some(manifest.id.clone()), - NotificationLevel::Error, - String::from("Install Failed"), - err_str, - (), - None, - ) - .await + } + .await { - tracing::error!("Failed to issue Notification: {}", e); - tracing::debug!("{:?}", e); + tracing::error!("Error tracking sideload progress: {e}"); + tracing::debug!("{e:?}"); } - } - }); - - if let Ok(_) = recv.await { - Response::builder() - .status(StatusCode::OK) - .body(Body::empty()) - .with_kind(ErrorKind::Network) - } else { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("installation aborted before upload completed")) - .with_kind(ErrorKind::Network) - } - } - .boxed() - }); - ctx.add_continuation( - guid.clone(), - RpcContinuation::rest(handler, Duration::from_secs(30)), - ) - .await; - Ok(guid) -} - -#[instrument(skip_all)] -async fn cli_install( - ctx: CliContext, - target: String, - marketplace_url: Option, - version_spec: Option, - version_priority: Option, -) -> Result<(), RpcError> { - if target.ends_with(".s9pk") { - let path = PathBuf::from(target); - - // inspect manifest no verify - let mut reader = S9pkReader::open(&path, false).await?; - let manifest = reader.manifest().await?; - let icon = reader.icon().await?.to_vec().await?; - let icon_str = format!( - "data:image/{};base64,{}", - manifest.assets.icon_type(), - base64::encode(&icon) - ); - - // rpc call remote sideload - tracing::debug!("calling package.sideload"); - let guid = rpc_toolkit::command_helpers::call_remote( - ctx.clone(), - "package.sideload", - serde_json::json!({ "manifest": manifest, "icon": icon_str }), - PhantomData::, - ) - .await? - .result?; - tracing::debug!("package.sideload succeeded {:?}", guid); - - // hit continuation api with guid that comes back - let file = tokio::fs::File::open(path).await?; - let content_length = file.metadata().await?.len(); - let body = Body::wrap_stream(tokio_util::io::ReaderStream::new(file)); - let res = ctx - .client - .post(format!("{}rest/rpc/{}", ctx.base_url, guid,)) - .header(CONTENT_LENGTH, content_length) - .body(body) - .send() - .await?; - if res.status().as_u16() == 200 { - tracing::info!("Package Uploaded") - } else { - tracing::info!("Package Upload failed: {}", res.text().await?) - } - } else { - let params = match (target.split_once("@"), version_spec) { - (Some((pkg, v)), None) => { - serde_json::json!({ "id": pkg, "marketplace-url": marketplace_url, "version-spec": v, "version-priority": version_priority }) - } - (Some(_), Some(_)) => { - return Err(crate::Error::new( - eyre!("Invalid package id {}", target), - ErrorKind::InvalidRequest, - ) - .into()) - } - (None, Some(v)) => { - serde_json::json!({ "id": target, "marketplace-url": marketplace_url, "version-spec": v, "version-priority": version_priority }) - } - (None, None) => { - serde_json::json!({ "id": target, "marketplace-url": marketplace_url, "version-priority": version_priority }) - } - }; - tracing::debug!("calling package.install"); - rpc_toolkit::command_helpers::call_remote( - ctx, - "package.install", - params, - PhantomData::<()>, + }, + Duration::from_secs(600), + ), ) - .await? - .result?; - tracing::debug!("package.install succeeded"); - } - Ok(()) -} - -#[command(display(display_none), metadata(sync_db = true))] -pub async fn uninstall( - #[context] ctx: RpcContext, - #[arg] id: PackageId, -) -> Result { - ctx.db - .mutate(|db| { - let (manifest, static_files, installed) = - match db.as_package_data().as_idx(&id).or_not_found(&id)?.de()? { - PackageDataEntry::Installed(PackageDataEntryInstalled { - manifest, - static_files, - installed, - }) => (manifest, static_files, installed), - _ => { - return Err(Error::new( - eyre!("Package is not installed."), - crate::ErrorKind::NotFound, - )); - } - }; - let pde = PackageDataEntry::Removing(PackageDataEntryRemoving { - manifest, - static_files, - removing: installed, - }); - db.as_package_data_mut().insert(&id, &pde) - }) - .await?; - - let return_id = id.clone(); - + .await; tokio::spawn(async move { if let Err(e) = async { - cleanup::uninstall(&ctx, ctx.secret_store.acquire().await?.as_mut(), &id).await + let key = ctx.db.peek().await.into_private().into_compat_s9pk_key(); + + ctx.services + .install( + ctx.clone(), + || crate::s9pk::load(file.clone(), || Ok(key.de()?.0), Some(&progress_tracker)), + None::, + Some(progress_tracker.clone()), + ) + .await? + .await? + .await?; + file.delete().await } .await { - let err_str = format!("Uninstall of {} Failed: {}", id, e); - tracing::error!("{}", err_str); - tracing::debug!("{:?}", e); - if let Err(e) = ctx - .notification_manager - .notify( - ctx.db.clone(), // allocating separate handle here because the lifetime of the previous one is the expression - Some(id), - NotificationLevel::Error, - String::from("Uninstall Failed"), - err_str, - (), - None, - ) - .await - { - tracing::error!("Failed to issue Notification: {}", e); - tracing::debug!("{:?}", e); - } + tracing::error!("Error sideloading package: {e}"); + tracing::debug!("{e:?}"); + let _ = err_send.send(e); } }); - - Ok(return_id) + Ok(SideloadResponse { upload, progress }) } -#[instrument(skip_all)] -pub async fn download_install_s9pk( - ctx: RpcContext, - temp_manifest: Manifest, - marketplace_url: Option, - progress: Arc, - mut s9pk: impl AsyncRead + Unpin, - download_complete: Option>, -) -> Result<(), Error> { - let pkg_id = &temp_manifest.id; - let version = &temp_manifest.version; - let db = ctx.db.peek().await; - - if let Result::<(), Error>::Err(e) = { - let ctx = ctx.clone(); - async move { - // // Build set of existing manifests - let mut manifests = Vec::new(); - for (_id, pkg) in db.as_package_data().as_entries()? { - let m = pkg.as_manifest().de()?; - manifests.push(m); - } - // Build map of current port -> ssl mappings - let port_map = ssl_port_status(&manifests); - tracing::info!("SSL Port Map: {:?}", &port_map); +#[derive(Deserialize, Serialize, Parser)] +pub struct QueryPackageParams { + id: PackageId, + version: Option, +} - // if any of the requested interface lan configs conflict with current state, fail the install - for (_id, iface) in &temp_manifest.interfaces.0 { - if let Some(cfg) = &iface.lan_config { - for (p, lan) in cfg { - if p.0 == 80 && lan.ssl || p.0 == 443 && !lan.ssl { - return Err(Error::new( - eyre!("SSL Conflict with StartOS"), - ErrorKind::LanPortConflict, - )); - } - match port_map.get(&p) { - Some((ssl, pkg)) => { - if *ssl != lan.ssl { - return Err(Error::new( - eyre!("SSL Conflict with package: {}", pkg), - ErrorKind::LanPortConflict, - )); - } - } - None => { - continue; - } +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CliInstallParams { + Marketplace(QueryPackageParams), + Sideload(PathBuf), +} +impl CommandFactory for CliInstallParams { + fn command() -> clap::Command { + use clap::{Arg, Command}; + Command::new("install") + .arg( + Arg::new("sideload") + .long("sideload") + .short('s') + .required_unless_present("id") + .value_parser(value_parser!(PathBuf)), + ) + .args( + QueryPackageParams::command() + .get_arguments() + .cloned() + .map(|a| { + if a.get_id() == "id" { + a.required(false).required_unless_present("sideload") + } else { + a } - } - } - } - - let pkg_archive_dir = ctx - .datadir - .join(PKG_ARCHIVE_DIR) - .join(pkg_id) - .join(version.as_str()); - tokio::fs::create_dir_all(&pkg_archive_dir).await?; - let pkg_archive = - pkg_archive_dir.join(AsRef::::as_ref(pkg_id).with_extension("s9pk")); - - File::delete(&pkg_archive).await?; - let mut dst = OpenOptions::new() - .create(true) - .write(true) - .read(true) - .open(&pkg_archive) - .await?; - - progress - .track_download_during(ctx.db.clone(), pkg_id, || async { - let mut progress_writer = - InstallProgressTracker::new(&mut dst, progress.clone()); - tokio::io::copy(&mut s9pk, &mut progress_writer).await?; - progress.download_complete(); - if let Some(complete) = download_complete { - complete.send(()).unwrap_or_default(); - } - Ok(()) - }) - .await?; - - dst.seek(SeekFrom::Start(0)).await?; - - let progress_reader = InstallProgressTracker::new(dst, progress.clone()); - let mut s9pk_reader = progress - .track_read_during(ctx.db.clone(), pkg_id, || { - S9pkReader::from_reader(progress_reader, true) - }) - .await?; - - install_s9pk( - ctx.clone(), - pkg_id, - version, - marketplace_url, - &mut s9pk_reader, - progress, + .conflicts_with("sideload") + }), ) - .await?; - - Ok(()) - } } - .await - { - if let Err(e) = cleanup_failed(&ctx, pkg_id).await { - tracing::error!("Failed to clean up {}@{}: {}", pkg_id, version, e); - tracing::debug!("{:?}", e); - } - - Err(e) - } else { - Ok::<_, Error>(()) + fn command_for_update() -> clap::Command { + Self::command() } } - -#[instrument(skip_all)] -pub async fn install_s9pk( - ctx: RpcContext, - pkg_id: &PackageId, - version: &Version, - marketplace_url: Option, - rdr: &mut S9pkReader>, - progress: Arc, -) -> Result<(), Error> { - rdr.validate().await?; - rdr.validated(); - let developer_key = rdr.developer_key().clone(); - rdr.reset().await?; - let db = ctx.db.peek().await; - - tracing::info!("Install {}@{}: Unpacking Manifest", pkg_id, version); - let manifest = progress - .track_read_during(ctx.db.clone(), pkg_id, || rdr.manifest()) - .await?; - tracing::info!("Install {}@{}: Unpacked Manifest", pkg_id, version); - - tracing::info!("Install {}@{}: Fetching Dependency Info", pkg_id, version); - let mut dependency_info = BTreeMap::new(); - for (dep, info) in &manifest.dependencies.0 { - let manifest: Option = if let Some(local_man) = db - .as_package_data() - .as_idx(dep) - .map(|pde| pde.as_manifest().de()) - { - Some(local_man?) - } else if let Some(marketplace_url) = &marketplace_url { - match ctx - .client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/manifest/{}?spec={}", - marketplace_url, dep, info.version, - ) - .parse()?, - )) - .send() - .await - .with_kind(crate::ErrorKind::Registry)? - .error_for_status() - { - Ok(a) => Ok(Some( - a.json() - .await - .with_kind(crate::ErrorKind::Deserialization)?, - )), - Err(e) - if e.status() == Some(StatusCode::BAD_REQUEST) - || e.status() == Some(StatusCode::NOT_FOUND) => - { - Ok(None) - } - Err(e) => Err(e), - } - .with_kind(crate::ErrorKind::Registry)? - } else { - None - }; - - let icon_path = if let Some(manifest) = &manifest { - let dir = ctx - .datadir - .join(PKG_PUBLIC_DIR) - .join(&manifest.id) - .join(manifest.version.as_str()); - let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type())); - if tokio::fs::metadata(&icon_path).await.is_err() { - if let Some(marketplace_url) = &marketplace_url { - tokio::fs::create_dir_all(&dir).await?; - let icon = ctx - .client - .get(with_query_params( - ctx.clone(), - format!( - "{}/package/v0/icon/{}?spec={}", - marketplace_url, dep, info.version, - ) - .parse()?, - )) - .send() - .await - .with_kind(crate::ErrorKind::Registry)?; - let mut dst = File::create(&icon_path).await?; - tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?; - dst.sync_all().await?; - Some(icon_path) - } else { - None - } - } else { - Some(icon_path) - } - } else { - None - }; - - dependency_info.insert( - dep.clone(), - StaticDependencyInfo { - title: manifest - .as_ref() - .map(|x| x.title.clone()) - .unwrap_or_else(|| dep.to_string()), - icon: if let Some(icon_path) = &icon_path { - DataUrl::from_path(icon_path).await? - } else { - DataUrl::from_slice("image/png", include_bytes!("./package-icon.png")) - }, - }, - ); - } - tracing::info!("Install {}@{}: Fetched Dependency Info", pkg_id, version); - - let icon = progress - .track_read_during(ctx.db.clone(), pkg_id, || { - unpack_s9pk(&ctx.datadir, &manifest, rdr) - }) - .await?; - - progress.unpack_complete.store(true, Ordering::SeqCst); - - progress - .track_read( - ctx.db.clone(), - pkg_id.clone(), - Arc::new(::std::sync::atomic::AtomicBool::new(true)), - ) - .await?; - - let peek = ctx.db.peek().await; - let prev = peek - .as_package_data() - .as_idx(pkg_id) - .or_not_found(pkg_id)? - .de()?; - let mut sql_tx = ctx.secret_store.begin().await?; - - tracing::info!("Install {}@{}: Creating volumes", pkg_id, version); - manifest.volumes.install(&ctx, pkg_id, version).await?; - tracing::info!("Install {}@{}: Created volumes", pkg_id, version); - - tracing::info!("Install {}@{}: Installing interfaces", pkg_id, version); - let interface_addresses = manifest.interfaces.install(sql_tx.as_mut(), pkg_id).await?; - tracing::info!( - "Install {}@{}: Installed interfaces {:?}", - pkg_id, - version, - interface_addresses - ); - - tracing::info!("Install {}@{}: Creating manager", pkg_id, version); - let manager = ctx.managers.add(ctx.clone(), manifest.clone()).await?; - tracing::info!("Install {}@{}: Created manager", pkg_id, version); - - let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type()); - let current_dependencies: CurrentDependencies = CurrentDependencies( - manifest - .dependencies - .0 - .iter() - .filter_map(|(id, info)| { - if info.requirement.required() { - Some((id.clone(), CurrentDependencyInfo::default())) - } else { - None - } - }) - .collect(), - ); - let mut dependents_static_dependency_info = BTreeMap::new(); - let current_dependents = { - let mut deps = BTreeMap::new(); - for package in db.as_package_data().keys()? { - if db - .as_package_data() - .as_idx(&package) - .or_not_found(&package)? - .as_installed() - .and_then(|i| i.as_dependency_info().as_idx(&pkg_id)) - .is_some() - { - dependents_static_dependency_info.insert(package.clone(), icon.clone()); - } - if let Some(dep) = db - .as_package_data() - .as_idx(&package) - .or_not_found(&package)? - .as_installed() - .and_then(|i| i.as_current_dependencies().as_idx(pkg_id)) - { - deps.insert(package, dep.de()?); - } - } - - CurrentDependents(deps) - }; - - let installed = InstalledPackageInfo { - status: Status { - configured: manifest.config.is_none(), - main: MainStatus::Stopped, - dependency_config_errors: compute_dependency_config_errs( - &ctx, - &peek, - &manifest, - ¤t_dependencies, - &Default::default(), - ) - .await?, - }, - marketplace_url, - developer_key, - manifest: manifest.clone(), - last_backup: match prev { - PackageDataEntry::Updating(PackageDataEntryUpdating { - installed: - InstalledPackageInfo { - last_backup: Some(time), - .. - }, - .. - }) => Some(time), - _ => None, - }, - dependency_info, - current_dependents: current_dependents.clone(), - current_dependencies: current_dependencies.clone(), - interface_addresses, - }; - let mut next = PackageDataEntryInstalled { - installed, - manifest: manifest.clone(), - static_files, - }; - - let mut auto_start = false; - let mut configured = false; - - let mut to_cleanup = None; - - if let PackageDataEntry::Updating(PackageDataEntryUpdating { - installed: prev, .. - }) = &prev - { - let prev_is_configured = prev.status.configured; - let prev_migration = prev - .manifest - .migrations - .to( - &ctx, - version, - pkg_id, - &prev.manifest.version, - &prev.manifest.volumes, - ) - .map(futures::future::Either::Left); - let migration = manifest - .migrations - .from( - &manifest.containers, - &ctx, - &prev.manifest.version, - pkg_id, - version, - &manifest.volumes, - ) - .map(futures::future::Either::Right); - - let viable_migration = if prev.manifest.version > manifest.version { - prev_migration.or(migration) +impl FromArgMatches for CliInstallParams { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + if let Some(sideload) = matches.get_one::("sideload") { + Ok(Self::Sideload(sideload.clone())) } else { - migration.or(prev_migration) - }; - - if let Some(f) = viable_migration { - configured = f.await?.configured && prev_is_configured; + Ok(Self::Marketplace(QueryPackageParams::from_arg_matches( + matches, + )?)) } - if configured || manifest.config.is_none() { - auto_start = prev.status.main.running(); - } - if &prev.manifest.version != version { - to_cleanup = Some((prev.manifest.id.clone(), prev.manifest.version.clone())); - } - } else if let PackageDataEntry::Restoring(PackageDataEntryRestoring { .. }) = prev { - next.installed.marketplace_url = manifest - .backup - .restore(&ctx, pkg_id, version, &manifest.volumes) - .await?; } - - sql_tx.commit().await?; - - let to_configure = ctx - .db - .mutate(|db| { - for (package, icon) in dependents_static_dependency_info { - db.as_package_data_mut() - .as_idx_mut(&package) - .or_not_found(&package)? - .as_installed_mut() - .or_not_found(&package)? - .as_dependency_info_mut() - .insert( - &pkg_id, - &StaticDependencyInfo { - icon, - title: manifest.title.clone(), - }, - )?; - } - db.as_package_data_mut() - .insert(&pkg_id, &PackageDataEntry::Installed(next))?; - if let PackageDataEntry::Updating(PackageDataEntryUpdating { - installed: prev, .. - }) = &prev - { - remove_from_current_dependents_lists(db, pkg_id, &prev.current_dependencies)?; - } - add_dependent_to_current_dependents_lists(db, pkg_id, ¤t_dependencies)?; - - set_dependents_with_live_pointers_to_needs_config(db, pkg_id) - }) - .await?; - - if let Some((id, version)) = to_cleanup { - cleanup(&ctx, &id, &version).await?; + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + *self = Self::from_arg_matches(matches)?; + Ok(()) } +} - if configured && manifest.config.is_some() { - let breakages = BTreeMap::new(); - let overrides = Default::default(); - - let configure_context = ConfigureContext { - breakages, - timeout: None, - config: None, - dry_run: false, - overrides, - }; - manager.configure(configure_context).await?; - } +#[derive(Deserialize, Serialize, Parser, TS)] +#[ts(export)] +pub struct InstalledVersionParams { + id: PackageId, +} - for to_configure in to_configure.into_iter().filter(|(dep, _)| dep != pkg_id) { - if let Err(e) = async { - ctx.managers - .get(&to_configure) - .await - .or_not_found(format!("manager for {}", to_configure.0))? - .configure(ConfigureContext { - breakages: BTreeMap::new(), - timeout: None, - config: None, - overrides: BTreeMap::new(), - dry_run: false, - }) - .await - } +pub async fn installed_version( + ctx: RpcContext, + InstalledVersionParams { id }: InstalledVersionParams, +) -> Result, Error> { + if let Some(pde) = ctx + .db + .peek() .await - { - tracing::error!("error configuring dependent: {e}"); - tracing::debug!("{e:?}") - } - } - - if auto_start { - manager.start().await; + .into_public() + .into_package_data() + .into_idx(&id) + { + Ok(Some( + pde.into_state_info() + .as_manifest(ManifestPreference::Old) + .as_version() + .de()?, + )) + } else { + Ok(None) } - - tracing::info!("Install {}@{}: Complete", pkg_id, version); - - Ok(()) } #[instrument(skip_all)] -pub async fn unpack_s9pk( - datadir: impl AsRef, - manifest: &Manifest, - rdr: &mut S9pkReader, -) -> Result, Error> { - let datadir = datadir.as_ref(); - let pkg_id = &manifest.id; - let version = &manifest.version; - - let public_dir_path = datadir - .join(PKG_PUBLIC_DIR) - .join(pkg_id) - .join(version.as_str()); - tokio::fs::create_dir_all(&public_dir_path).await?; - - tracing::info!("Install {}@{}: Unpacking LICENSE.md", pkg_id, version); - let license_path = public_dir_path.join("LICENSE.md"); - let mut dst = File::create(&license_path).await?; - tokio::io::copy(&mut rdr.license().await?, &mut dst).await?; - dst.sync_all().await?; - tracing::info!("Install {}@{}: Unpacked LICENSE.md", pkg_id, version); - - tracing::info!("Install {}@{}: Unpacking INSTRUCTIONS.md", pkg_id, version); - let instructions_path = public_dir_path.join("INSTRUCTIONS.md"); - let mut dst = File::create(&instructions_path).await?; - tokio::io::copy(&mut rdr.instructions().await?, &mut dst).await?; - dst.sync_all().await?; - tracing::info!("Install {}@{}: Unpacked INSTRUCTIONS.md", pkg_id, version); - - let icon_filename = Path::new("icon").with_extension(manifest.assets.icon_type()); - let icon_path = public_dir_path.join(&icon_filename); - tracing::info!( - "Install {}@{}: Unpacking {}", - pkg_id, - version, - icon_path.display() - ); - let icon_buf = rdr.icon().await?.to_vec().await?; - let mut dst = File::create(&icon_path).await?; - dst.write_all(&icon_buf).await?; - dst.sync_all().await?; - let icon = DataUrl::from_vec( - mime(manifest.assets.icon_type()).unwrap_or("image/png"), - icon_buf, - ); - tracing::info!( - "Install {}@{}: Unpacked {}", - pkg_id, - version, - icon_filename.display() - ); +pub async fn cli_install( + HandlerArgs { + context: ctx, + parent_method, + method, + params, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { + let method = parent_method.into_iter().chain(method).collect_vec(); + match params { + CliInstallParams::Sideload(path) => { + let file = open_file(path).await?; + + // rpc call remote sideload + let SideloadResponse { upload, progress } = from_value::( + ctx.call_remote::( + &method[..method.len() - 1] + .into_iter() + .chain(std::iter::once(&"sideload")) + .join("."), + imbl_value::json!({}), + ) + .await?, + )?; + + let upload = async { + let content_length = file.metadata().await?.len(); + ctx.rest_continuation( + upload, + reqwest::Body::wrap_stream(tokio_util::io::ReaderStream::new(file)), + { + let mut map = HeaderMap::new(); + map.insert(CONTENT_LENGTH, content_length.into()); + map + }, + ) + .await? + .error_for_status() + .with_kind(ErrorKind::Network)?; + Ok::<_, Error>(()) + }; - tracing::info!("Install {}@{}: Unpacking Docker Images", pkg_id, version); - Command::new(CONTAINER_TOOL) - .arg("load") - .input(Some(&mut rdr.docker_images().await?)) - .invoke(ErrorKind::Docker) - .await?; - tracing::info!("Install {}@{}: Unpacked Docker Images", pkg_id, version,); + let progress = async { + use tokio_tungstenite::tungstenite::Message; - tracing::info!("Install {}@{}: Unpacking Assets", pkg_id, version); - let asset_dir = asset_dir(datadir, pkg_id, version); - if tokio::fs::metadata(&asset_dir).await.is_ok() { - tokio::fs::remove_dir_all(&asset_dir).await?; - } - tokio::fs::create_dir_all(&asset_dir).await?; - let mut tar = tokio_tar::Archive::new(rdr.assets().await?); - tar.unpack(asset_dir).await?; + let mut bar = PhasedProgressBar::new("Sideloading"); - let script_dir = script_dir(datadir, pkg_id, version); - if tokio::fs::metadata(&script_dir).await.is_err() { - tokio::fs::create_dir_all(&script_dir).await?; - } - if let Some(mut hdl) = rdr.scripts().await? { - tokio::io::copy( - &mut hdl, - &mut File::create(script_dir.join("embassy.js")).await?, - ) - .await?; - } - tracing::info!("Install {}@{}: Unpacked Assets", pkg_id, version); + let mut ws = ctx.ws_continuation(progress).await?; - Ok(icon) -} + let mut progress = FullProgress::new(); -#[instrument(skip_all)] -pub fn rebuild_from<'a>( - source: impl AsRef + 'a + Send + Sync, - datadir: impl AsRef + 'a + Send + Sync, -) -> BoxFuture<'a, Result<(), Error>> { - async move { - let source_dir = source.as_ref(); - let datadir = datadir.as_ref(); - if tokio::fs::metadata(&source_dir).await.is_ok() { - ReadDirStream::new(tokio::fs::read_dir(&source_dir).await?) - .map(|r| { - r.with_ctx(|_| (crate::ErrorKind::Filesystem, format!("{:?}", &source_dir))) - }) - .try_for_each(|entry| async move { - let m = entry.metadata().await?; - if m.is_file() { - let path = entry.path(); - let ext = path.extension().and_then(|ext| ext.to_str()); - if ext == Some("tar") || ext == Some("s9pk") { - if let Err(e) = async { - match ext { - Some("tar") => { - Command::new(CONTAINER_TOOL) - .arg("load") - .input(Some(&mut File::open(&path).await?)) - .invoke(ErrorKind::Docker) - .await?; - Ok::<_, Error>(()) + loop { + tokio::select! { + msg = ws.next() => { + if let Some(msg) = msg { + match msg.with_kind(ErrorKind::Network)? { + Message::Text(t) => { + progress = + serde_json::from_str::(&t) + .with_kind(ErrorKind::Deserialization)?; + bar.update(&progress); } - Some("s9pk") => { - let mut s9pk = S9pkReader::open(&path, true).await?; - unpack_s9pk(datadir, &s9pk.manifest().await?, &mut s9pk) - .await?; - Ok(()) + Message::Close(Some(c)) if c.code != CloseCode::Normal => { + return Err(Error::new(eyre!("{}", c.reason), ErrorKind::Network)) } - _ => unreachable!(), + _ => (), } + } else { + break; } - .await - { - tracing::error!("Error unpacking {path:?}: {e}"); - tracing::debug!("{e:?}"); + } + _ = tokio::time::sleep(Duration::from_millis(100)) => { + bar.update(&progress); + }, + } + } + + Ok::<_, Error>(()) + }; + + let (upload, progress) = tokio::join!(upload, progress); + progress?; + upload?; + } + CliInstallParams::Marketplace(QueryPackageParams { id, version }) => { + let source_version: Option = from_value( + ctx.call_remote::("package.installed-version", json!({ "id": &id })) + .await?, + )?; + let mut packages: GetPackageResponse = from_value( + ctx.call_remote::( + "package.get", + json!({ "id": &id, "version": version, "sourceVersion": source_version }), + ) + .await?, + )?; + let version = if packages.best.len() == 1 { + packages.best.pop_first().map(|(k, _)| k).unwrap() + } else { + println!("Multiple flavors of {id} found. Please select one of the following versions to install:"); + let version; + loop { + let (mut read, mut output) = rustyline_async::Readline::new("> ".into()) + .with_kind(ErrorKind::Filesystem)?; + for (idx, version) in packages.best.keys().enumerate() { + output + .write_all(format!(" {}) {}\n", idx + 1, version).as_bytes()) + .await?; + read.add_history_entry(version.to_string()); + } + if let ReadlineEvent::Line(line) = read.readline().await? { + let trimmed = line.trim(); + match trimmed.parse() { + Ok(v) => { + if let Some((k, _)) = packages.best.remove_entry(&v) { + version = k; + break; + } } - Ok(()) - } else { - Ok(()) + Err(_) => match trimmed.parse::() { + Ok(i) if (1..=packages.best.len()).contains(&i) => { + version = packages.best.keys().nth(i - 1).unwrap().clone(); + break; + } + _ => (), + }, } - } else if m.is_dir() { - rebuild_from(entry.path(), datadir).await?; - Ok(()) + eprintln!("invalid selection: {trimmed}"); + println!("Please select one of the following versions to install:"); } else { - Ok(()) + return Err(Error::new( + eyre!("Could not determine precise version to install"), + ErrorKind::InvalidRequest, + ) + .into()); } - }) - .await - } else { - Ok(()) + } + version + }; + ctx.call_remote::( + &method.join("."), + to_value(&InstallParams { + id, + registry: ctx.registry_url.clone().or_not_found("--registry")?, + version, + })?, + ) + .await?; } } - .boxed() + Ok(()) } -fn ssl_port_status(manifests: &Vec) -> BTreeMap { - let mut ret = BTreeMap::new(); - for m in manifests { - for (_id, iface) in &m.interfaces.0 { - match &iface.lan_config { - None => {} - Some(cfg) => { - for (p, lan) in cfg { - ret.insert(p.clone(), (lan.ssl, m.id.clone())); - } - } - } +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct UninstallParams { + id: PackageId, +} + +pub async fn uninstall( + ctx: RpcContext, + UninstallParams { id }: UninstallParams, +) -> Result { + ctx.db + .mutate(|db| { + let entry = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)?; + entry.as_state_info_mut().map_mutate(|s| match s { + PackageState::Installed(s) => Ok(PackageState::Removing(s)), + _ => Err(Error::new( + eyre!("Package {id} is not installed."), + crate::ErrorKind::NotFound, + )), + }) + }) + .await?; + + let return_id = id.clone(); + + tokio::spawn(async move { + if let Err(e) = ctx.services.uninstall(&ctx, &id).await { + tracing::error!("Error uninstalling service {id}: {e}"); + tracing::debug!("{e:?}"); } - } - ret + }); + + Ok(return_id) } diff --git a/core/startos/src/install/progress.rs b/core/startos/src/install/progress.rs deleted file mode 100644 index 61e58e0e6..000000000 --- a/core/startos/src/install/progress.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::future::Future; -use std::io::SeekFrom; -use std::pin::Pin; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; - -use models::{OptionExt, PackageId}; -use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; - -use crate::db::model::Database; -use crate::prelude::*; - -#[derive(Debug, Deserialize, Serialize, HasModel, Default)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InstallProgress { - pub size: Option, - pub downloaded: AtomicU64, - pub download_complete: AtomicBool, - pub validated: AtomicU64, - pub validation_complete: AtomicBool, - pub unpacked: AtomicU64, - pub unpack_complete: AtomicBool, -} -impl InstallProgress { - pub fn new(size: Option) -> Self { - InstallProgress { - size, - downloaded: AtomicU64::new(0), - download_complete: AtomicBool::new(false), - validated: AtomicU64::new(0), - validation_complete: AtomicBool::new(false), - unpacked: AtomicU64::new(0), - unpack_complete: AtomicBool::new(false), - } - } - pub fn download_complete(&self) { - self.download_complete.store(true, Ordering::SeqCst) - } - pub async fn track_download(self: Arc, db: PatchDb, id: PackageId) -> Result<(), Error> { - let update = |d: &mut Model| { - d.as_package_data_mut() - .as_idx_mut(&id) - .or_not_found(&id)? - .as_install_progress_mut() - .or_not_found("install-progress")? - .ser(&self) - }; - while !self.download_complete.load(Ordering::SeqCst) { - db.mutate(&update).await?; - tokio::time::sleep(Duration::from_millis(300)).await; - } - db.mutate(&update).await - } - pub async fn track_download_during< - F: FnOnce() -> Fut, - Fut: Future>, - T, - >( - self: &Arc, - db: PatchDb, - id: &PackageId, - f: F, - ) -> Result { - let tracker = tokio::spawn(self.clone().track_download(db.clone(), id.clone())); - let res = f().await; - self.download_complete.store(true, Ordering::SeqCst); - tracker.await.unwrap()?; - res - } - pub async fn track_read( - self: Arc, - db: PatchDb, - id: PackageId, - complete: Arc, - ) -> Result<(), Error> { - let update = |d: &mut Model| { - d.as_package_data_mut() - .as_idx_mut(&id) - .or_not_found(&id)? - .as_install_progress_mut() - .or_not_found("install-progress")? - .ser(&self) - }; - while !complete.load(Ordering::SeqCst) { - db.mutate(&update).await?; - tokio::time::sleep(Duration::from_millis(300)).await; - } - db.mutate(&update).await - } - pub async fn track_read_during< - F: FnOnce() -> Fut, - Fut: Future>, - T, - >( - self: &Arc, - db: PatchDb, - id: &PackageId, - f: F, - ) -> Result { - let complete = Arc::new(AtomicBool::new(false)); - let tracker = tokio::spawn(self.clone().track_read( - db.clone(), - id.clone(), - complete.clone(), - )); - let res = f().await; - complete.store(true, Ordering::SeqCst); - tracker.await.unwrap()?; - res - } -} - -#[pin_project::pin_project] -#[derive(Debug)] -pub struct InstallProgressTracker { - #[pin] - inner: RW, - validating: bool, - progress: Arc, -} -impl InstallProgressTracker { - pub fn new(inner: RW, progress: Arc) -> Self { - InstallProgressTracker { - inner, - validating: true, - progress, - } - } - pub fn validated(&mut self) { - self.progress - .validation_complete - .store(true, Ordering::SeqCst); - self.validating = false; - } -} -impl AsyncWrite for InstallProgressTracker { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let this = self.project(); - match this.inner.poll_write(cx, buf) { - Poll::Ready(Ok(n)) => { - this.progress - .downloaded - .fetch_add(n as u64, Ordering::SeqCst); - Poll::Ready(Ok(n)) - } - a => a, - } - } - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - this.inner.poll_flush(cx) - } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - let this = self.project(); - this.inner.poll_shutdown(cx) - } - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> Poll> { - let this = self.project(); - match this.inner.poll_write_vectored(cx, bufs) { - Poll::Ready(Ok(n)) => { - this.progress - .downloaded - .fetch_add(n as u64, Ordering::SeqCst); - Poll::Ready(Ok(n)) - } - a => a, - } - } -} -impl AsyncRead for InstallProgressTracker { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - let this = self.project(); - let prev = buf.filled().len() as u64; - match this.inner.poll_read(cx, buf) { - Poll::Ready(Ok(())) => { - if *this.validating { - &this.progress.validated - } else { - &this.progress.unpacked - } - .fetch_add(buf.filled().len() as u64 - prev, Ordering::SeqCst); - - Poll::Ready(Ok(())) - } - a => a, - } - } -} -impl AsyncSeek for InstallProgressTracker { - fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { - let this = self.project(); - this.inner.start_seek(position) - } - fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - match this.inner.poll_complete(cx) { - Poll::Ready(Ok(n)) => { - if *this.validating { - &this.progress.validated - } else { - &this.progress.unpacked - } - .store(n, Ordering::SeqCst); - Poll::Ready(Ok(n)) - } - a => a, - } - } -} diff --git a/core/startos/src/install/update.rs b/core/startos/src/install/update.rs index 694051213..a0374fc80 100644 --- a/core/startos/src/install/update.rs +++ b/core/startos/src/install/update.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use models::PackageId; use rpc_toolkit::command; use tracing::instrument; @@ -7,7 +8,6 @@ use crate::config::not_found; use crate::context::RpcContext; use crate::db::model::CurrentDependents; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; use crate::util::serde::display_serializable; use crate::util::Version; use crate::Error; diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 5fde6513f..bcdcb7f91 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -1,13 +1,13 @@ -pub const DEFAULT_MARKETPLACE: &str = "https://registry.start9.com"; +use const_format::formatcp; + +pub const DATA_DIR: &str = "/media/startos/data"; +pub const MAIN_DATA: &str = formatcp!("{DATA_DIR}/main"); +pub const PACKAGE_DATA: &str = formatcp!("{DATA_DIR}/package-data"); +pub const DEFAULT_REGISTRY: &str = "https://registry.start9.com"; // pub const COMMUNITY_MARKETPLACE: &str = "https://community-registry.start9.com"; -pub const BUFFER_SIZE: usize = 1024; -pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; -pub const TARGET: &str = current_platform::CURRENT_PLATFORM; +pub const HOST_IP: [u8; 4] = [10, 0, 3, 1]; +pub use std::env::consts::ARCH; lazy_static::lazy_static! { - pub static ref ARCH: &'static str = { - let (arch, _) = TARGET.split_once("-").unwrap(); - arch - }; pub static ref PLATFORM: String = { if let Ok(platform) = std::fs::read_to_string("/usr/lib/startos/PLATFORM.txt") { platform @@ -20,15 +20,22 @@ lazy_static::lazy_static! { }; } +mod cap { + #![allow(non_upper_case_globals)] + + pub const CAP_1_KiB: usize = 1024; + pub const CAP_1_MiB: usize = CAP_1_KiB * CAP_1_KiB; + pub const CAP_10_MiB: usize = 10 * CAP_1_MiB; +} +pub use cap::*; + pub mod account; pub mod action; pub mod auth; pub mod backup; pub mod bins; -pub mod config; pub mod context; pub mod control; -pub mod core; pub mod db; pub mod dependencies; pub mod developer; @@ -38,20 +45,19 @@ pub mod error; pub mod firmware; pub mod hostname; pub mod init; -pub mod inspect; pub mod install; pub mod logs; -pub mod manager; +pub mod lxc; pub mod middleware; -pub mod migration; pub mod net; pub mod notifications; pub mod os_install; pub mod prelude; -pub mod procedure; -pub mod properties; +pub mod progress; pub mod registry; +pub mod rpc_continuations; pub mod s9pk; +pub mod service; pub mod setup; pub mod shutdown; pub mod sound; @@ -59,100 +65,493 @@ pub mod ssh; pub mod status; pub mod system; pub mod update; +pub mod upload; pub mod util; pub mod version; pub mod volume; use std::time::SystemTime; -pub use config::Config; +use clap::Parser; pub use error::{Error, ErrorKind, ResultExt}; -use rpc_toolkit::command; +use imbl_value::Value; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{ + from_fn, from_fn_async, from_fn_blocking, CallRemoteHandler, Context, Empty, HandlerExt, + ParentHandler, +}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::{ + CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext, +}; +use crate::disk::fsck::RequiresReboot; +use crate::net::net; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; +use crate::util::serde::HandlerExtSerde; -#[command(metadata(authenticated = false))] -pub fn echo(#[arg] message: String) -> Result { +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[ts(export)] +pub struct EchoParams { + message: String, +} + +pub fn echo(_: C, EchoParams { message }: EchoParams) -> Result { Ok(message) } -#[command(subcommands( - version::git_info, - echo, - inspect::inspect, - server, - package, - net::net, - auth::auth, - db::db, - ssh::ssh, - net::wifi::wifi, - disk::disk, - notifications::notification, - backup::backup, - registry::marketplace::marketplace, -))] -pub fn main_api() -> Result<(), RpcError> { - Ok(()) +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum ApiState { + Error, + Initializing, + Running, +} +impl std::fmt::Display for ApiState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self, f) + } +} + +pub fn main_api() -> ParentHandler { + let api = ParentHandler::new() + .subcommand( + "git-info", + from_fn(|_: C| version::git_info()).with_about("Display the githash of StartOS CLI"), + ) + .subcommand( + "echo", + from_fn(echo::) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Echo a message") + .with_call_remote::(), + ) + .subcommand( + "state", + from_fn(|_: RpcContext| Ok::<_, Error>(ApiState::Running)) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the API that is currently serving") + .with_call_remote::(), + ) + .subcommand( + "server", + server::() + .with_about("Commands related to the server i.e. restart, update, and shutdown"), + ) + .subcommand( + "package", + package::().with_about("Commands related to packages"), + ) + .subcommand( + "net", + net::net::().with_about("Network commands related to tor and dhcp"), + ) + .subcommand( + "auth", + auth::auth::().with_about( + "Commands related to Authentication i.e. login, logout, reset-password", + ), + ) + .subcommand( + "db", + db::db::().with_about("Commands to interact with the db i.e. dump, put, apply"), + ) + .subcommand( + "ssh", + ssh::ssh::() + .with_about("Commands for interacting with ssh keys i.e. add, delete, list"), + ) + .subcommand( + "wifi", + net::wifi::wifi::() + .with_about("Commands related to wifi networks i.e. add, connect, delete"), + ) + .subcommand( + "disk", + disk::disk::().with_about("Commands for listing disk info and repairing"), + ) + .subcommand( + "notification", + notifications::notification::().with_about("Create, delete, or list notifications"), + ) + .subcommand( + "backup", + backup::backup::() + .with_about("Commands related to backup creation and backup targets"), + ) + .subcommand( + "registry", + CallRemoteHandler::::new( + registry::registry_api::(), + ) + .no_cli(), + ) + .subcommand( + "s9pk", + s9pk::rpc::s9pk().with_about("Commands for interacting with s9pk files"), + ) + .subcommand( + "util", + util::rpc::util::().with_about("Command for calculating the blake3 hash of a file"), + ); + #[cfg(feature = "dev")] + let api = api.subcommand( + "lxc", + lxc::dev::lxc::() + .with_about("Commands related to lxc containers i.e. create, list, remove, connect"), + ); + api +} + +pub fn server() -> ParentHandler { + ParentHandler::new() + .subcommand( + "time", + from_fn_async(system::time) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(system::display_time(handle.params, result)) + }) + .with_about("Display current time and server uptime") + .with_call_remote::() + ) + .subcommand( + "experimental", + system::experimental::() + .with_about("Commands related to configuring experimental options such as zram and cpu governor"), + ) + .subcommand( + "logs", + system::logs::().with_about("Display OS logs"), + ) + .subcommand( + "logs", + from_fn_async(logs::cli_logs::).no_display().with_about("Display OS logs"), + ) + .subcommand( + "kernel-logs", + system::kernel_logs::().with_about("Display Kernel logs"), + ) + .subcommand( + "kernel-logs", + from_fn_async(logs::cli_logs::).no_display().with_about("Display Kernel logs"), + ) + .subcommand( + "metrics", + from_fn_async(system::metrics) + .with_display_serializable() + .with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage") + .with_call_remote::() + ) + .subcommand( + "shutdown", + from_fn_async(shutdown::shutdown) + .no_display() + .with_about("Shutdown the server") + .with_call_remote::() + ) + .subcommand( + "restart", + from_fn_async(shutdown::restart) + .no_display() + .with_about("Restart the server") + .with_call_remote::() + ) + .subcommand( + "rebuild", + from_fn_async(shutdown::rebuild) + .no_display() + .with_about("Teardown and rebuild service containers") + .with_call_remote::() + ) + .subcommand( + "update", + from_fn_async(update::update_system) + .with_metadata("sync_db", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "update", + from_fn_async(update::cli_update_system).no_display().with_about("Check a given registry for StartOS updates and update if available"), + ) + .subcommand( + "update-firmware", + from_fn_async(|_: RpcContext| async { + if let Some(firmware) = firmware::check_for_firmware_update().await? { + firmware::update_firmware(firmware).await?; + Ok::<_, Error>(RequiresReboot(true)) + } else { + Ok(RequiresReboot(false)) + } + }) + .with_custom_display_fn(|_handle, result| { + Ok(firmware::display_firmware_update_result(result)) + }) + .with_about("Update the mainboard's firmware to the latest firmware available in this version of StartOS if available. Note: This command does not reach out to the Internet") + .with_call_remote::() + ) + .subcommand( + "set-smtp", + from_fn_async(system::set_system_smtp) + .no_display() + .with_about("Set system smtp server and credentials") + .with_call_remote::() + ) + .subcommand( + "test-smtp", + from_fn_async(system::test_smtp) + .no_display() + .with_about("Send test email using provided smtp server and credentials") + .with_call_remote::() + ) + .subcommand( + "clear-smtp", + from_fn_async(system::clear_system_smtp) + .no_display() + .with_about("Remove system smtp server and credentials") + .with_call_remote::() + ).subcommand("host", net::host::server_host_api::().with_about("Commands for modifying the host for the system ui")) } -#[command(subcommands( - system::time, - system::experimental, - system::logs, - system::kernel_logs, - system::metrics, - shutdown::shutdown, - shutdown::restart, - shutdown::rebuild, - update::update_system, - firmware::update_firmware, -))] -pub fn server() -> Result<(), RpcError> { - Ok(()) +pub fn package() -> ParentHandler { + ParentHandler::new() + .subcommand( + "action", + action::action_api::().with_about("Commands to get action input or run an action"), + ) + .subcommand( + "install", + from_fn_async(install::install) + .with_metadata("sync_db", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "sideload", + from_fn_async(install::sideload) + .with_metadata("get_session", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "install", + from_fn_async(install::cli_install) + .no_display() + .with_about("Install a package from a marketplace or via sideloading"), + ) + .subcommand( + "uninstall", + from_fn_async(install::uninstall) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Remove a package") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(install::list) + .with_display_serializable() + .with_about("List installed packages") + .with_call_remote::(), + ) + .subcommand( + "installed-version", + from_fn_async(install::installed_version) + .with_display_serializable() + .with_about("Display installed version for a PackageId") + .with_call_remote::(), + ) + .subcommand( + "start", + from_fn_async(control::start) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Start a service") + .with_call_remote::(), + ) + .subcommand( + "stop", + from_fn_async(control::stop) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Stop a service") + .with_call_remote::(), + ) + .subcommand( + "restart", + from_fn_async(control::restart) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Restart a service") + .with_call_remote::(), + ) + .subcommand( + "rebuild", + from_fn_async(service::rebuild) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Rebuild service container") + .with_call_remote::(), + ) + .subcommand("logs", logs::package_logs()) + .subcommand( + "logs", + logs::package_logs().with_about("Display package logs"), + ) + .subcommand( + "logs", + from_fn_async(logs::cli_logs::) + .no_display() + .with_about("Display package logs"), + ) + .subcommand( + "backup", + backup::package_backup::() + .with_about("Commands for restoring package(s) from backup"), + ) + .subcommand("connect", from_fn_async(service::connect_rpc).no_cli()) + .subcommand( + "connect", + from_fn_async(service::connect_rpc_cli) + .no_display() + .with_about("Connect to a LXC container"), + ) + .subcommand( + "attach", + from_fn_async(service::attach) + .with_metadata("get_session", Value::Bool(true)) + .with_about("Execute commands within a service container") + .no_cli(), + ) + .subcommand("attach", from_fn_async(service::cli_attach).no_display()) + .subcommand( + "host", + net::host::host_api::().with_about("Manage network hosts for a package"), + ) } -#[command(subcommands( - action::action, - install::install, - install::sideload, - install::uninstall, - install::list, - config::config, - control::start, - control::stop, - control::restart, - logs::logs, - properties::properties, - dependencies::dependency, - backup::package_backup, -))] -pub fn package() -> Result<(), RpcError> { - Ok(()) +pub fn diagnostic_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "git-info", + from_fn(|_: DiagnosticContext| version::git_info()) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the githash of StartOS CLI"), + ) + .subcommand( + "echo", + from_fn(echo::) + .with_about("Echo a message") + .with_call_remote::(), + ) + .subcommand( + "state", + from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error)) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the API that is currently serving") + .with_call_remote::(), + ) + .subcommand( + "diagnostic", + diagnostic::diagnostic::() + .with_about("Diagnostic commands i.e. logs, restart, rebuild"), + ) } -#[command(subcommands( - version::git_info, - s9pk::pack, - developer::verify, - developer::init, - inspect::inspect, - registry::admin::publish, -))] -pub fn portable_api() -> Result<(), RpcError> { - Ok(()) +pub fn init_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "git-info", + from_fn(|_: InitContext| version::git_info()) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the githash of StartOS CLI"), + ) + .subcommand( + "echo", + from_fn(echo::) + .with_about("Echo a message") + .with_call_remote::(), + ) + .subcommand( + "state", + from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing)) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the API that is currently serving") + .with_call_remote::(), + ) + .subcommand( + "init", + init::init_api::() + .with_about("Commands to get logs or initialization progress"), + ) } -#[command(subcommands(version::git_info, echo, diagnostic::diagnostic))] -pub fn diagnostic_api() -> Result<(), RpcError> { - Ok(()) +pub fn setup_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "git-info", + from_fn(|_: SetupContext| version::git_info()) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the githash of StartOS CLI"), + ) + .subcommand( + "echo", + from_fn(echo::) + .with_about("Echo a message") + .with_call_remote::(), + ) + .subcommand("setup", setup::setup::()) } -#[command(subcommands(version::git_info, echo, setup::setup))] -pub fn setup_api() -> Result<(), RpcError> { - Ok(()) +pub fn install_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "git-info", + from_fn(|_: InstallContext| version::git_info()) + .with_metadata("authenticated", Value::Bool(false)) + .with_about("Display the githash of StartOS CLI"), + ) + .subcommand( + "echo", + from_fn(echo::) + .with_about("Echo a message") + .with_call_remote::(), + ) + .subcommand( + "install", + os_install::install::() + .with_about("Commands to list disk info, install StartOS, and reboot"), + ) } -#[command(subcommands(version::git_info, echo, os_install::install))] -pub fn install_api() -> Result<(), RpcError> { - Ok(()) +pub fn expanded_api() -> ParentHandler { + main_api() + .subcommand( + "init", + from_fn_blocking(developer::init) + .no_display() + .with_about("Create developer key if it doesn't exist"), + ) + .subcommand( + "pubkey", + from_fn_blocking(developer::pubkey) + .with_about("Get public key for developer private key"), + ) + .subcommand( + "diagnostic", + diagnostic::diagnostic::() + .with_about("Commands to display logs, restart the server, etc"), + ) + .subcommand("setup", setup::setup::()) + .subcommand( + "install", + os_install::install::() + .with_about("Commands to list disk info, install StartOS, and reboot"), + ) + .subcommand( + "registry", + registry::registry_api::().with_about("Commands related to the registry"), + ) } diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 691ae09b9..be5e9cb8f 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -1,40 +1,42 @@ -use std::future::Future; -use std::marker::PhantomData; +use std::convert::Infallible; use std::ops::{Deref, DerefMut}; use std::process::Stdio; +use std::str::FromStr; use std::time::{Duration, UNIX_EPOCH}; +use axum::extract::ws::{self, WebSocket}; use chrono::{DateTime, Utc}; +use clap::builder::ValueParserFactory; +use clap::{Args, FromArgMatches, Parser}; use color_eyre::eyre::eyre; use futures::stream::BoxStream; -use futures::{FutureExt, SinkExt, Stream, StreamExt, TryStreamExt}; -use hyper::upgrade::Upgraded; -use hyper::Error as HyperError; -use rpc_toolkit::command; +use futures::{Future, FutureExt, Stream, StreamExt, TryStreamExt}; +use itertools::Itertools; +use models::{FromStrParser, PackageId}; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{ + from_fn_async, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler, +}; +use serde::de::{self, DeserializeOwned}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; -use tokio::task::JoinError; use tokio_stream::wrappers::LinesStream; -use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; -use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::Message; -use tokio_tungstenite::WebSocketStream; use tracing::instrument; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::error::ResultExt; -use crate::procedure::docker::DockerProcedure; -use crate::s9pk::manifest::PackageId; -use crate::util::display_none; +use crate::lxc::ContainerId; +use crate::prelude::*; +use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; +use crate::util::net::WebSocketExt; use crate::util::serde::Reversible; -use crate::{Error, ErrorKind}; +use crate::util::Invoke; #[pin_project::pin_project] pub struct LogStream { - _child: Child, + _child: Option, #[pin] entries: BoxStream<'static, Result>, } @@ -65,74 +67,64 @@ impl Stream for LogStream { } #[instrument(skip_all)] -async fn ws_handler< - WSFut: Future, HyperError>, JoinError>>, ->( +async fn ws_handler( first_entry: Option, mut logs: LogStream, - ws_fut: WSFut, + mut stream: WebSocket, ) -> Result<(), Error> { - let mut stream = ws_fut - .await - .with_kind(crate::ErrorKind::Network)? - .with_kind(crate::ErrorKind::Unknown)?; - if let Some(first_entry) = first_entry { stream - .send(Message::Text( + .send(ws::Message::Text( serde_json::to_string(&first_entry).with_kind(ErrorKind::Serialization)?, )) .await .with_kind(ErrorKind::Network)?; } - let mut ws_closed = false; - while let Some(entry) = tokio::select! { - a = logs.try_next() => Some(a?), - a = stream.try_next() => { a.with_kind(crate::ErrorKind::Network)?; ws_closed = true; None } - } { - if let Some(entry) = entry { - let (_, log_entry) = entry.log_entry()?; - stream - .send(Message::Text( - serde_json::to_string(&log_entry).with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; + loop { + tokio::select! { + entry = logs.try_next() => { + if let Some(entry) = entry? { + let (_, log_entry) = entry.log_entry()?; + stream + .send(ws::Message::Text( + serde_json::to_string(&log_entry).with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } else { + return stream.normal_close("complete").await; + } + }, + msg = stream.try_next() => { + if msg.with_kind(crate::ErrorKind::Network)?.is_none() { + return Ok(()) + } + } } } - - if !ws_closed { - stream - .close(Some(CloseFrame { - code: CloseCode::Normal, - reason: "Log Stream Finished".into(), - })) - .await - .with_kind(ErrorKind::Network)?; - } - - Ok(()) } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct LogResponse { - entries: Reversible, + pub entries: Reversible, start_cursor: Option, end_cursor: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct LogFollowResponse { start_cursor: Option, - guid: RequestGuid, + guid: Guid, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct LogEntry { timestamp: DateTime, message: String, + boot_id: String, } impl std::fmt::Display for LogEntry { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -155,6 +147,8 @@ pub struct JournalctlEntry { pub message: String, #[serde(rename = "__CURSOR")] pub cursor: String, + #[serde(rename = "_BOOT_ID")] + pub boot_id: String, } impl JournalctlEntry { fn log_entry(self) -> Result<(String, LogEntry), Error> { @@ -165,6 +159,7 @@ impl JournalctlEntry { UNIX_EPOCH + Duration::from_micros(self.timestamp.parse::()?), ), message: self.message, + boot_id: self.boot_id, }, )) } @@ -183,7 +178,13 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( where E: serde::de::Error, { - Ok(v.trim().to_owned()) + Ok(v.to_owned()) + } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(v) } fn visit_unit(self) -> Result where @@ -201,7 +202,7 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( .flatten() .collect::, _>>()?, ) - .map(|s| s.trim().to_owned()) + .map(|s| s.to_owned()) .map_err(serde::de::Error::custom) } } @@ -214,153 +215,400 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( /// --user-unit=UNIT Show logs from the specified user unit)) /// System: Unit is startd, but we also filter on the comm /// Container: Filtering containers, like podman/docker is done by filtering on the CONTAINER_NAME -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum LogSource { Kernel, Unit(&'static str), - System, - Container(PackageId), + Container(ContainerId), } pub const SYSTEM_UNIT: &str = "startd"; -#[command( - custom_cli(cli_logs(async, context(CliContext))), - subcommands(self(logs_nofollow(async)), logs_follow), - display(display_none) -)] -pub async fn logs( - #[arg] id: PackageId, - #[arg(short = 'l', long = "limit")] limit: Option, - #[arg(short = 'c', long = "cursor")] cursor: Option, - #[arg(short = 'B', long = "before", default)] before: bool, - #[arg(short = 'f', long = "follow", default)] follow: bool, -) -> Result<(PackageId, Option, Option, bool, bool), Error> { - Ok((id, limit, cursor, before, follow)) -} -pub async fn cli_logs( - ctx: CliContext, - (id, limit, cursor, before, follow): (PackageId, Option, Option, bool, bool), -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct PackageIdParams { + id: PackageId, +} + +#[derive(Debug, Clone)] +pub enum BootIdentifier { + Index(i32), + Id(String), +} +impl FromStr for BootIdentifier { + type Err = Infallible; + fn from_str(s: &str) -> Result { + Ok(match s.parse() { + Ok(i) => Self::Index(i), + Err(_) => Self::Id(s.to_owned()), + }) + } +} +impl ValueParserFactory for BootIdentifier { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} +impl Serialize for BootIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Index(i) => serializer.serialize_i32(*i), + Self::Id(i) => serializer.serialize_str(i), } - cli_logs_generic_follow(ctx, "package.logs.follow", Some(id), limit).await - } else { - cli_logs_generic_nofollow(ctx, "package.logs", Some(id), limit, cursor, before).await } } -pub async fn logs_nofollow( - _ctx: (), - (id, limit, cursor, before, _): (PackageId, Option, Option, bool, bool), -) -> Result { - fetch_logs(LogSource::Container(id), limit, cursor, before).await +impl<'de> Deserialize<'de> for BootIdentifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = BootIdentifier; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a string or integer") + } + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(Self::Value::Id(v.to_owned())) + } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(Self::Value::Id(v)) + } + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + } + deserializer.deserialize_any(Visitor) + } } -#[command(rpc_only, rename = "follow", display(display_none))] -pub async fn logs_follow( - #[context] ctx: RpcContext, - #[parent_data] (id, limit, _, _, _): (PackageId, Option, Option, bool, bool), -) -> Result { - follow_logs(ctx, LogSource::Container(id), limit).await +impl From for String { + fn from(value: BootIdentifier) -> Self { + match value { + BootIdentifier::Index(i) => i.to_string(), + BootIdentifier::Id(i) => i, + } + } } -pub async fn cli_logs_generic_nofollow( - ctx: CliContext, - method: &str, - id: Option, +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct LogsParams { + #[command(flatten)] + #[serde(flatten)] + extra: Extra, + #[arg(short = 'l', long = "limit")] limit: Option, + #[arg(short = 'c', long = "cursor", conflicts_with = "follow")] cursor: Option, + #[arg(short = 'b', long = "boot")] + #[serde(default)] + boot: Option, + #[arg(short = 'B', long = "before", conflicts_with = "follow")] + #[serde(default)] before: bool, -) -> Result<(), RpcError> { - let res = rpc_toolkit::command_helpers::call_remote( - ctx.clone(), - method, - serde_json::json!({ - "id": id, - "limit": limit, - "cursor": cursor, - "before": before, - }), - PhantomData::, - ) - .await? - .result?; +} - for entry in res.entries.iter() { - println!("{}", entry); - } +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliLogsParams { + #[command(flatten)] + #[serde(flatten)] + rpc_params: LogsParams, + #[arg(short = 'f', long = "follow")] + #[serde(default)] + follow: bool, +} - Ok(()) +#[allow(private_bounds)] +pub fn logs< + C: Context + AsRef, + Extra: FromArgMatches + Serialize + DeserializeOwned + Args + Send + Sync + 'static, +>( + source: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> ParentHandler> { + ParentHandler::new() + .root_handler(logs_nofollow::(source.clone()).no_cli()) + .subcommand( + "follow", + logs_follow::(source) + .with_inherited(|params, _| params) + .no_cli(), + ) } -pub async fn cli_logs_generic_follow( - ctx: CliContext, - method: &str, - id: Option, - limit: Option, -) -> Result<(), RpcError> { - let res = rpc_toolkit::command_helpers::call_remote( - ctx.clone(), +pub async fn cli_logs( + HandlerArgs { + context: ctx, + parent_method, method, - serde_json::json!({ - "id": id, - "limit": limit, - }), - PhantomData::, - ) - .await? - .result?; - - let mut base_url = ctx.base_url.clone(); - let ws_scheme = match base_url.scheme() { - "https" => "wss", - "http" => "ws", - _ => { - return Err(Error::new( - eyre!("Cannot parse scheme from base URL"), - crate::ErrorKind::ParseUrl, - ) - .into()) + params: CliLogsParams { rpc_params, follow }, + .. + }: HandlerArgs>, +) -> Result<(), RpcError> +where + CliContext: CallRemote, + Extra: FromArgMatches + Args + Serialize + Send + Sync, +{ + let method = parent_method + .into_iter() + .chain(method) + .chain(follow.then_some("follow")) + .join("."); + + if follow { + let res = from_value::( + ctx.call_remote::(&method, to_value(&rpc_params)?) + .await?, + )?; + + let mut stream = ctx.ws_continuation(res.guid).await?; + while let Some(log) = stream.try_next().await? { + if let Message::Text(log) = log { + println!("{}", serde_json::from_str::(&log)?); + } } - }; - base_url - .set_scheme(ws_scheme) - .map_err(|_| Error::new(eyre!("Cannot set URL scheme"), crate::ErrorKind::ParseUrl))?; - let (mut stream, _) = - // base_url is "http://127.0.0.1/", with a trailing slash, so we don't put a leading slash in this path: - tokio_tungstenite::connect_async(format!("{}ws/rpc/{}", base_url, res.guid)).await?; - while let Some(log) = stream.try_next().await? { - if let Message::Text(log) = log { - println!("{}", serde_json::from_str::(&log)?); + } else { + let res = from_value::( + ctx.call_remote::(&method, to_value(&rpc_params)?) + .await?, + )?; + + for entry in res.entries.iter() { + println!("{}", entry); } } Ok(()) } +trait LogSourceFn<'a, Context, Extra>: Clone + Send + Sync + 'static { + type Fut: Future> + Send + 'a; + fn call(&self, ctx: &'a Context, extra: Extra) -> Self::Fut; +} + +impl<'a, C: Context, Extra, F, Fut> LogSourceFn<'a, C, Extra> for F +where + F: Fn(&'a C, Extra) -> Fut + Clone + Send + Sync + 'static, + Fut: Future> + Send + 'a, +{ + type Fut = Fut; + fn call(&self, ctx: &'a C, extra: Extra) -> Self::Fut { + self(ctx, extra) + } +} + +fn logs_nofollow( + f: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> impl HandlerFor, InheritedParams = Empty, Ok = LogResponse, Err = Error> +where + C: Context, + Extra: FromArgMatches + Args + Send + Sync + 'static, +{ + from_fn_async( + move |HandlerArgs { + context, + params: + LogsParams { + extra, + limit, + cursor, + boot, + before, + }, + .. + }: HandlerArgs>| { + let f = f.clone(); + async move { + fetch_logs( + f.call(&context, extra).await?, + limit, + cursor, + boot.map(String::from), + before, + ) + .await + } + }, + ) +} + +fn logs_follow< + C: Context + AsRef, + Extra: FromArgMatches + Args + Send + Sync + 'static, +>( + f: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> impl HandlerFor< + C, + Params = Empty, + InheritedParams = LogsParams, + Ok = LogFollowResponse, + Err = Error, +> { + from_fn_async( + move |HandlerArgs { + context, + inherited_params: + LogsParams { + extra, + cursor, + limit, + boot, + .. + }, + .. + }: HandlerArgs>| { + let f = f.clone(); + async move { + let src = f.call(&context, extra).await?; + follow_logs(context, src, cursor, limit, boot.map(String::from)).await + } + }, + ) +} + +async fn get_package_id( + ctx: &RpcContext, + PackageIdParams { id }: PackageIdParams, +) -> Result { + let container_id = ctx + .services + .get(&id) + .await + .as_ref() + .map(|x| x.container_id()) + .ok_or_else(|| { + Error::new( + eyre!("No service found with id: {}", id), + ErrorKind::NotFound, + ) + })??; + Ok(LogSource::Container(container_id)) +} + +pub fn package_logs() -> ParentHandler> { + logs::(get_package_id) +} + pub async fn journalctl( id: LogSource, - limit: usize, + limit: Option, cursor: Option<&str>, + boot: Option<&str>, before: bool, follow: bool, ) -> Result { - let mut cmd = Command::new("journalctl"); + let mut cmd = gen_journalctl_command(&id); + + if let Some(limit) = limit { + cmd.arg(format!("--lines={}", limit)); + } + + if let Some(cursor) = cursor { + cmd.arg(&format!("--after-cursor={}", cursor)); + if before { + cmd.arg("--reverse"); + } + } + + if let Some(boot) = boot { + cmd.arg(format!("--boot={boot}")); + } else { + cmd.arg("--boot=all"); + } + + let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)? + .lines() + .map(serde_json::from_str::) + .collect::, _>>() + .with_kind(ErrorKind::Deserialization)?; + + if follow { + let mut follow_cmd = gen_journalctl_command(&id); + follow_cmd.arg("-f"); + if let Some(last) = deserialized_entries.last() { + follow_cmd.arg(format!("--after-cursor={}", last.cursor)); + follow_cmd.arg("--lines=all"); + } else { + follow_cmd.arg("--lines=0"); + } + let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?; + let out = + BufReader::new(child.stdout.take().ok_or_else(|| { + Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald) + })?); + + let journalctl_entries = LinesStream::new(out.lines()); + + let follow_deserialized_entries = journalctl_entries + .map_err(|e| Error::new(e, crate::ErrorKind::Journald)) + .and_then(|s| { + futures::future::ready( + serde_json::from_str::(&s) + .with_kind(crate::ErrorKind::Deserialization), + ) + }); + + let entries = futures::stream::iter(deserialized_entries) + .map(Ok) + .chain(follow_deserialized_entries) + .boxed(); + Ok(LogStream { + _child: Some(child), + entries, + }) + } else { + let entries = futures::stream::iter(deserialized_entries).map(Ok).boxed(); + + Ok(LogStream { + _child: None, + entries, + }) + } +} + +fn gen_journalctl_command(id: &LogSource) -> Command { + let mut cmd = match id { + LogSource::Container(container_id) => { + let mut cmd = Command::new("lxc-attach"); + cmd.arg(format!("{}", container_id)) + .arg("--") + .arg("journalctl"); + cmd + } + _ => Command::new("journalctl"), + }; cmd.kill_on_drop(true); cmd.arg("--output=json"); cmd.arg("--output-fields=MESSAGE"); - cmd.arg(format!("-n{}", limit)); match id { LogSource::Kernel => { cmd.arg("-k"); @@ -369,59 +617,11 @@ pub async fn journalctl( cmd.arg("-u"); cmd.arg(id); } - LogSource::System => { - cmd.arg("-u"); - cmd.arg(SYSTEM_UNIT); - cmd.arg(format!("_COMM={}", SYSTEM_UNIT)); - } - LogSource::Container(id) => { - #[cfg(not(feature = "docker"))] - cmd.arg(format!( - "SYSLOG_IDENTIFIER={}", - DockerProcedure::container_name(&id, None) - )); - #[cfg(feature = "docker")] - cmd.arg(format!( - "CONTAINER_NAME={}", - DockerProcedure::container_name(&id, None) - )); + LogSource::Container(_container_id) => { + cmd.arg("-u").arg("container-runtime.service"); } }; - - let cursor_formatted = format!("--after-cursor={}", cursor.unwrap_or("")); - if cursor.is_some() { - cmd.arg(&cursor_formatted); - if before { - cmd.arg("--reverse"); - } - } - if follow { - cmd.arg("--follow"); - } - - let mut child = cmd.stdout(Stdio::piped()).spawn()?; - let out = BufReader::new( - child - .stdout - .take() - .ok_or_else(|| Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald))?, - ); - - let journalctl_entries = LinesStream::new(out.lines()); - - let deserialized_entries = journalctl_entries - .map_err(|e| Error::new(e, crate::ErrorKind::Journald)) - .and_then(|s| { - futures::future::ready( - serde_json::from_str::(&s) - .with_kind(crate::ErrorKind::Deserialization), - ) - }); - - Ok(LogStream { - _child: child, - entries: deserialized_entries.boxed(), - }) + cmd } #[instrument(skip_all)] @@ -429,10 +629,19 @@ pub async fn fetch_logs( id: LogSource, limit: Option, cursor: Option, + boot: Option, before: bool, ) -> Result { let limit = limit.unwrap_or(50); - let mut stream = journalctl(id, limit, cursor.as_deref(), before, false).await?; + let mut stream = journalctl( + id, + Some(limit), + cursor.as_deref(), + boot.as_deref(), + before, + false, + ) + .await?; let mut entries = Vec::with_capacity(limit); let mut start_cursor = None; @@ -472,13 +681,19 @@ pub async fn fetch_logs( } #[instrument(skip_all)] -pub async fn follow_logs( - ctx: RpcContext, +pub async fn follow_logs>( + ctx: Context, id: LogSource, + cursor: Option, limit: Option, + boot: Option, ) -> Result { - let limit = limit.unwrap_or(50); - let mut stream = journalctl(id, limit, None, false, true).await?; + let limit = if cursor.is_some() { + None + } else { + Some(limit.unwrap_or(50)) + }; + let mut stream = journalctl(id, limit, cursor.as_deref(), boot.as_deref(), false, true).await?; let mut start_cursor = None; let mut first_entry = None; @@ -494,15 +709,25 @@ pub async fn follow_logs( first_entry = Some(entry); } - let guid = RequestGuid::new(); - ctx.add_continuation( - guid.clone(), - RpcContinuation::ws( - Box::new(move |ws_fut| ws_handler(first_entry, stream, ws_fut).boxed()), - Duration::from_secs(30), - ), - ) - .await; + let guid = Guid::new(); + ctx.as_ref() + .add( + guid.clone(), + RpcContinuation::ws( + Box::new(move |socket| { + ws_handler(first_entry, stream, socket) + .map(|x| match x { + Ok(_) => (), + Err(e) => { + tracing::error!("Error in log stream: {}", e); + } + }) + .boxed() + }), + Duration::from_secs(30), + ), + ) + .await; Ok(LogFollowResponse { start_cursor, guid }) } diff --git a/core/startos/src/lxc/config.template b/core/startos/src/lxc/config.template new file mode 100644 index 000000000..a85b700e4 --- /dev/null +++ b/core/startos/src/lxc/config.template @@ -0,0 +1,19 @@ +# Distribution configuration +lxc.include = /usr/share/lxc/config/common.conf +lxc.include = /usr/share/lxc/config/userns.conf +lxc.arch = linux64 + +# Container specific configuration +lxc.apparmor.profile = generated +lxc.apparmor.allow_nesting = 1 +lxc.idmap = u 0 100000 65536 +lxc.idmap = g 0 100000 65536 +lxc.rootfs.path = dir:/var/lib/lxc/{guid}/rootfs +lxc.uts.name = {guid} + +# Network configuration +lxc.net.0.type = veth +lxc.net.0.link = lxcbr0 +lxc.net.0.flags = up + +lxc.rootfs.options = rshared diff --git a/core/startos/src/lxc/dev.rs b/core/startos/src/lxc/dev.rs new file mode 100644 index 000000000..a918672da --- /dev/null +++ b/core/startos/src/lxc/dev.rs @@ -0,0 +1,174 @@ +use std::ops::Deref; + +use clap::Parser; +use rpc_toolkit::{ + from_fn_async, CallRemoteHandler, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, + ParentHandler, +}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::lxc::{ContainerId, LxcConfig}; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::ServiceStats; + +pub fn lxc() -> ParentHandler { + ParentHandler::new() + .subcommand( + "create", + from_fn_async(create) + .with_about("Create lxc container") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list) + .with_custom_display_fn(|_, res| { + use prettytable::*; + let mut table = table!([bc => "GUID"]); + for guid in res { + table.add_row(row![&*guid]); + } + table.printstd(); + Ok(()) + }) + .with_about("List lxc containers") + .with_call_remote::(), + ) + .subcommand( + "stats", + from_fn_async(stats) + .with_custom_display_fn(|_, res| { + use prettytable::*; + let mut table = table!([ + "Container ID", + "Name", + "Memory Usage", + "Memory Limit", + "Memory %" + ]); + for ServiceStats { + container_id, + package_id, + memory_usage, + memory_limit, + } in res + { + table.add_row(row![ + &*container_id, + &*package_id, + memory_usage, + memory_limit, + format!( + "{:.2}", + memory_usage.0 as f64 / memory_limit.0 as f64 * 100.0 + ) + ]); + } + table.printstd(); + Ok(()) + }) + .with_about("List information related to the lxc containers i.e. CPU, Memory, Disk") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove) + .no_display() + .with_about("Remove lxc container") + .with_call_remote::(), + ) + .subcommand("connect", from_fn_async(connect_rpc).no_cli()) + .subcommand( + "connect", + from_fn_async(connect_rpc_cli) + .no_display() + .with_about("Connect to a lxc container"), + ) +} + +pub async fn create(ctx: RpcContext) -> Result { + let container = ctx.lxc_manager.create(None, LxcConfig::default()).await?; + let guid = container.guid.deref().clone(); + ctx.dev.lxc.lock().await.insert(guid.clone(), container); + Ok(guid) +} + +pub async fn list(ctx: RpcContext) -> Result, Error> { + Ok(ctx.dev.lxc.lock().await.keys().cloned().collect()) +} + +pub async fn stats(ctx: RpcContext) -> Result, Error> { + let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; + let guids: Vec<_> = ctx.dev.lxc.lock().await.keys().cloned().collect(); + + let mut stats = Vec::with_capacity(guids.len()); + for id in ids { + let service: tokio::sync::OwnedRwLockReadGuard> = + ctx.services.get(&id).await; + + let service_ref = service.as_ref().or_not_found(&id)?; + + stats.push(service_ref.stats().await?); + } + Ok(stats) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct RemoveParams { + #[ts(type = "string")] + pub guid: ContainerId, +} + +pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Result<(), Error> { + if let Some(container) = ctx.dev.lxc.lock().await.remove(&guid) { + container.exit().await?; + } + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct ConnectParams { + #[ts(type = "string")] + pub guid: ContainerId, +} + +pub async fn connect_rpc( + ctx: RpcContext, + ConnectParams { guid }: ConnectParams, +) -> Result { + super::connect( + &ctx, + ctx.dev.lxc.lock().await.get(&guid).ok_or_else(|| { + Error::new(eyre!("No container with guid: {guid}"), ErrorKind::NotFound) + })?, + ) + .await +} + +pub async fn connect_rpc_cli( + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgs, +) -> Result<(), Error> { + let ctx = context.clone(); + let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: rpc_toolkit::util::Flat(params, Empty {}), + inherited_params, + raw_params, + }) + .await?; + + super::connect_cli(&ctx, guid).await +} diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs new file mode 100644 index 000000000..ce5d28970 --- /dev/null +++ b/core/startos/src/lxc/mod.rs @@ -0,0 +1,571 @@ +use std::collections::BTreeSet; +use std::net::Ipv4Addr; +use std::path::Path; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +use clap::builder::ValueParserFactory; +use futures::{AsyncWriteExt, StreamExt}; +use imbl_value::{InOMap, InternedString}; +use models::{FromStrParser, InvalidId}; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; +use rustyline_async::{ReadlineEvent, SharedWriter}; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; +use tokio::sync::Mutex; +use tokio::time::Instant; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::disk::mount::filesystem::bind::Bind; +use crate::disk::mount::filesystem::block_dev::BlockDev; +use crate::disk::mount::filesystem::idmapped::IdMapped; +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::disk::mount::filesystem::{MountType, ReadOnly, ReadWrite}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; +use crate::disk::mount::util::unmount; +use crate::prelude::*; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::util::io::open_file; +use crate::util::rpc_client::UnixRpcClient; +use crate::util::{new_guid, Invoke}; + +// #[cfg(feature = "dev")] +pub mod dev; + +const LXC_CONTAINER_DIR: &str = "/var/lib/lxc"; +const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path +pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path +pub const HOST_RPC_SERVER_SOCKET: &str = "host.sock"; // must not be absolute path +const CONTAINER_DHCP_TIMEOUT: Duration = Duration::from_secs(30); + +#[derive( + Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS, +)] +#[ts(type = "string")] +pub struct ContainerId(InternedString); +impl std::ops::Deref for ContainerId { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::fmt::Display for ContainerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &*self.0) + } +} +impl TryFrom<&str> for ContainerId { + type Error = InvalidId; + fn try_from(value: &str) -> Result { + Ok(ContainerId(InternedString::intern(value))) + } +} +impl std::str::FromStr for ContainerId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} +impl ValueParserFactory for ContainerId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Default)] +pub struct LxcManager { + containers: Mutex>>, +} + +impl LxcManager { + pub fn new() -> Self { + Self::default() + } + + pub async fn create( + self: &Arc, + log_mount: Option<&Path>, + config: LxcConfig, + ) -> Result { + let container = LxcContainer::new(self, log_mount, config).await?; + let mut guard = self.containers.lock().await; + *guard = std::mem::take(&mut *guard) + .into_iter() + .filter(|g| g.strong_count() > 0) + .chain(std::iter::once(Arc::downgrade(&container.guid))) + .collect(); + Ok(container) + } + + pub async fn gc(&self) -> Result<(), Error> { + let expected = BTreeSet::from_iter( + self.containers + .lock() + .await + .iter() + .filter_map(|g| g.upgrade()) + .map(|g| (*g).clone()), + ); + for container in String::from_utf8( + Command::new("lxc-ls") + .arg("-1") + .invoke(ErrorKind::Lxc) + .await?, + )? + .lines() + .map(|s| s.trim()) + { + if !expected.contains(&ContainerId::try_from(container)?) { + let rootfs_path = Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"); + if tokio::fs::metadata(&rootfs_path).await.is_ok() { + unmount( + Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"), + true, + ) + .await + .log_err(); + if tokio_stream::wrappers::ReadDirStream::new( + tokio::fs::read_dir(&rootfs_path).await?, + ) + .count() + .await + > 0 + { + return Err(Error::new( + eyre!("rootfs is not empty, refusing to delete"), + ErrorKind::InvalidRequest, + )); + } + } + Command::new("lxc-destroy") + .arg("--force") + .arg("--name") + .arg(container) + .invoke(ErrorKind::Lxc) + .await?; + } + } + Ok(()) + } +} + +pub struct LxcContainer { + manager: Weak, + rootfs: OverlayGuard, + pub guid: Arc, + rpc_bind: TmpMountGuard, + log_mount: Option, + config: LxcConfig, + exited: bool, +} +impl LxcContainer { + async fn new( + manager: &Arc, + log_mount: Option<&Path>, + config: LxcConfig, + ) -> Result { + let guid = new_guid(); + let machine_id = hex::encode(rand::random::<[u8; 16]>()); + let container_dir = Path::new(LXC_CONTAINER_DIR).join(&*guid); + tokio::fs::create_dir_all(&container_dir).await?; + tokio::fs::write( + container_dir.join("config"), + format!(include_str!("./config.template"), guid = &*guid), + ) + .await?; + // TODO: append config + let rootfs_dir = container_dir.join("rootfs"); + tokio::fs::create_dir_all(&rootfs_dir).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&rootfs_dir) + .invoke(ErrorKind::Filesystem) + .await?; + let rootfs = OverlayGuard::mount( + TmpMountGuard::mount( + &IdMapped::new( + BlockDev::new("/usr/lib/startos/container-runtime/rootfs.squashfs"), + 0, + 100000, + 65536, + ), + ReadOnly, + ) + .await?, + &rootfs_dir, + ) + .await?; + tokio::fs::write(rootfs_dir.join("etc/machine-id"), format!("{machine_id}\n")).await?; + tokio::fs::write(rootfs_dir.join("etc/hostname"), format!("{guid}\n")).await?; + Command::new("sed") + .arg("-i") + .arg(format!("s/LXC_NAME/{guid}/g")) + .arg(rootfs_dir.join("etc/hosts")) + .invoke(ErrorKind::Filesystem) + .await?; + Command::new("mount") + .arg("--make-rshared") + .arg(rootfs.path()) + .invoke(ErrorKind::Filesystem) + .await?; + let rpc_dir = rootfs_dir.join(RPC_DIR); + tokio::fs::create_dir_all(&rpc_dir).await?; + let rpc_bind = TmpMountGuard::mount(&Bind::new(rpc_dir), ReadWrite).await?; + Command::new("chown") + .arg("-R") + .arg("100000:100000") + .arg(rpc_bind.path()) + .invoke(ErrorKind::Filesystem) + .await?; + let log_mount = if let Some(path) = log_mount { + let log_mount_point = rootfs_dir.join("var/log/journal").join(machine_id); + let log_mount = + MountGuard::mount(&Bind::new(path), &log_mount_point, MountType::ReadWrite).await?; + Command::new("chown") + // This was needed as 100999 because the group id of journald + .arg("100000:100999") + .arg(&log_mount_point) + .invoke(crate::ErrorKind::Filesystem) + .await?; + Some(log_mount) + } else { + None + }; + Command::new("lxc-start") + .arg("-d") + .arg("--name") + .arg(&*guid) + .invoke(ErrorKind::Lxc) + .await?; + Ok(Self { + manager: Arc::downgrade(manager), + rootfs, + guid: Arc::new(ContainerId::try_from(&*guid)?), + rpc_bind, + config, + exited: false, + log_mount, + }) + } + + pub fn rootfs_dir(&self) -> &Path { + self.rootfs.path() + } + + pub async fn ip(&self) -> Result { + let start = Instant::now(); + let guid: &str = &self.guid; + loop { + let output = String::from_utf8( + Command::new("lxc-info") + .arg("--name") + .arg(guid) + .arg("-iH") + .invoke(ErrorKind::Docker) + .await?, + )?; + for line in output.lines() { + if let Ok(ip) = line.trim().parse() { + return Ok(ip); + } + } + if start.elapsed() > CONTAINER_DHCP_TIMEOUT { + return Err(Error::new( + eyre!("Timed out waiting for container to acquire DHCP lease"), + ErrorKind::Timeout, + )); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + pub fn rpc_dir(&self) -> &Path { + self.rpc_bind.path() + } + + pub async fn command(&self, commands: &[&str]) -> Result { + let mut cmd = Command::new("lxc-attach"); + cmd.kill_on_drop(true); + + let output = cmd + .arg(&**self.guid) + .arg("--") + .args(commands) + .output() + .await?; + + if !output.status.success() { + return Err(Error::new( + eyre!( + "Command failed with exit code: {:?} \n Message: {:?}", + output.status.code(), + String::from_utf8(output.stderr) + ), + ErrorKind::Docker, + )); + } + Ok(String::from_utf8(output.stdout)?) + } + + #[instrument(skip_all)] + pub async fn exit(mut self) -> Result<(), Error> { + Command::new("lxc-stop") + .arg("--name") + .arg(&**self.guid) + .invoke(ErrorKind::Lxc) + .await?; + self.rpc_bind.take().unmount().await?; + if let Some(log_mount) = self.log_mount.take() { + log_mount.unmount(true).await?; + } + self.rootfs.take().unmount(true).await?; + let rootfs_path = self.rootfs_dir(); + if tokio::fs::metadata(&rootfs_path).await.is_ok() + && tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(&rootfs_path).await?) + .count() + .await + > 0 + { + return Err(Error::new( + eyre!("rootfs is not empty, refusing to delete"), + ErrorKind::InvalidRequest, + )); + } + Command::new("lxc-destroy") + .arg("--force") + .arg("--name") + .arg(&**self.guid) + .invoke(ErrorKind::Lxc) + .await?; + + self.exited = true; + + Ok(()) + } + + pub async fn connect_rpc(&self, timeout: Option) -> Result { + let started = Instant::now(); + let sock_path = self.rpc_dir().join(CONTAINER_RPC_SERVER_SOCKET); + while tokio::fs::metadata(&sock_path).await.is_err() { + if timeout.map_or(false, |t| started.elapsed() > t) { + return Err(Error::new( + eyre!("timed out waiting for socket"), + ErrorKind::Timeout, + )); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + Ok(UnixRpcClient::new(sock_path)) + } +} +impl Drop for LxcContainer { + fn drop(&mut self) { + if !self.exited { + tracing::warn!( + "Container {} was ungracefully dropped. Cleaning up dangling containers...", + &**self.guid + ); + let rootfs = self.rootfs.take(); + let guid = std::mem::take(&mut self.guid); + if let Some(manager) = self.manager.upgrade() { + tokio::spawn(async move { + if let Err(e) = async { + let err_path = rootfs.path().join("var/log/containerRuntime.err"); + if tokio::fs::metadata(&err_path).await.is_ok() { + let mut lines = BufReader::new(open_file(&err_path).await?).lines(); + while let Some(line) = lines.next_line().await? { + let container = &**guid; + tracing::error!(container, "{}", line); + } + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error reading logs from crashed container: {e}"); + tracing::debug!("{e:?}") + } + rootfs.unmount(true).await.log_err(); + drop(guid); + if let Err(e) = manager.gc().await { + tracing::error!("Error cleaning up dangling LXC containers: {e}"); + tracing::debug!("{e:?}") + } else { + tracing::info!("Successfully cleaned up dangling LXC containers"); + } + }); + } + } + } +} + +#[derive(Default, Serialize)] +pub struct LxcConfig {} +pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result { + use axum::extract::ws::Message; + + let rpc = container.connect_rpc(Some(Duration::from_secs(30))).await?; + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + if let Err(e) = async { + loop { + match ws.next().await { + None => break, + Some(Ok(Message::Text(txt))) => { + let mut id = None; + let result = async { + let req: RpcRequest = + serde_json::from_str(&txt).map_err(|e| RpcError { + data: Some(serde_json::Value::String( + e.to_string(), + )), + ..rpc_toolkit::yajrc::PARSE_ERROR + })?; + id = req.id; + rpc.request(req.method, req.params).await + } + .await; + ws.send(Message::Text( + serde_json::to_string(&RpcResponse { id, result }) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + Some(Ok(_)) => (), + Some(Err(e)) => { + return Err(Error::new(e, ErrorKind::Network)); + } + } + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + Ok(guid) +} + +pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> { + use futures::SinkExt; + use tokio_tungstenite::tungstenite::Message; + + let mut ws = ctx.ws_continuation(guid).await?; + let (mut input, mut output) = + rustyline_async::Readline::new("> ".into()).with_kind(ErrorKind::Filesystem)?; + + async fn handle_message( + msg: Option>, + output: &mut SharedWriter, + ) -> Result { + match msg { + None => return Ok(true), + Some(Ok(Message::Text(txt))) => match serde_json::from_str::(&txt) { + Ok(RpcResponse { result: Ok(a), .. }) => { + output + .write_all( + (serde_json::to_string(&a).with_kind(ErrorKind::Serialization)? + "\n") + .as_bytes(), + ) + .await?; + } + Ok(RpcResponse { result: Err(e), .. }) => { + let e: Error = e.into(); + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + Err(e) => { + tracing::error!("Error Parsing RPC response: {e}"); + tracing::debug!("{e:?}"); + } + }, + Some(Ok(_)) => (), + Some(Err(e)) => { + return Err(Error::new(e, ErrorKind::Network)); + } + }; + Ok(false) + } + + loop { + tokio::select! { + line = input.readline() => { + let line = line.with_kind(ErrorKind::Filesystem)?; + if let ReadlineEvent::Line(line) = line { + input.add_history_entry(line.clone()); + if serde_json::from_str::(&line).is_ok() { + ws.send(Message::Text(line)) + .await + .with_kind(ErrorKind::Network)?; + } else { + match shell_words::split(&line) { + Ok(command) => { + if let Some((method, rest)) = command.split_first() { + let mut params = InOMap::new(); + for arg in rest { + if let Some((name, value)) = arg.split_once('=') { + params.insert(InternedString::intern(name), if value.is_empty() { + Value::Null + } else if let Ok(v) = serde_json::from_str(value) { + v + } else { + Value::String(Arc::new(value.into())) + }); + } else { + tracing::error!("argument without a value: {arg}"); + tracing::debug!("help: set the value of {arg} with `{arg}=...`"); + continue; + } + } + ws.send(Message::Text(match serde_json::to_string(&RpcRequest { + id: None, + method: GenericRpcMethod::new(method.into()), + params: Value::Object(params), + }) { + Ok(a) => a, + Err(e) => { + tracing::error!("Error Serializing Request: {e}"); + tracing::debug!("{e:?}"); + continue; + } + })).await.with_kind(ErrorKind::Network)?; + if handle_message(ws.next().await, &mut output).await? { + break + } + } + } + Err(e) => { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + } + } + } else { + ws.send(Message::Close(None)).await.with_kind(ErrorKind::Network)?; + } + } + msg = ws.next() => { + if handle_message(msg, &mut output).await? { + break; + } + } + } + } + + Ok(()) +} diff --git a/core/startos/src/manager/health.rs b/core/startos/src/manager/health.rs deleted file mode 100644 index 30f18051a..000000000 --- a/core/startos/src/manager/health.rs +++ /dev/null @@ -1,56 +0,0 @@ -use models::OptionExt; -use tracing::instrument; - -use crate::context::RpcContext; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::status::MainStatus; -use crate::Error; - -/// So, this is used for a service to run a health check cycle, go out and run the health checks, and store those in the db -#[instrument(skip_all)] -pub async fn check(ctx: &RpcContext, id: &PackageId) -> Result<(), Error> { - let (manifest, started) = { - let peeked = ctx.db.peek().await; - let pde = peeked - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .expect_as_installed()?; - - let manifest = pde.as_installed().as_manifest().de()?; - - let started = pde.as_installed().as_status().as_main().de()?.started(); - - (manifest, started) - }; - - let health_results = if let Some(started) = started { - tracing::debug!("Checking health of {}", id); - manifest - .health_checks - .check_all(ctx, started, id, &manifest.version, &manifest.volumes) - .await? - } else { - return Ok(()); - }; - - ctx.db - .mutate(|v| { - let pde = v - .as_package_data_mut() - .as_idx_mut(id) - .or_not_found(id)? - .expect_as_installed_mut()?; - let status = pde.as_installed_mut().as_status_mut().as_main_mut(); - - if let MainStatus::Running { health: _, started } = status.de()? { - status.ser(&MainStatus::Running { - health: health_results.clone(), - started, - })?; - } - Ok(()) - }) - .await -} diff --git a/core/startos/src/manager/manager_container.rs b/core/startos/src/manager/manager_container.rs deleted file mode 100644 index 32e11c2e5..000000000 --- a/core/startos/src/manager/manager_container.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use models::OptionExt; -use tokio::sync::watch; -use tokio::sync::watch::Sender; -use tracing::instrument; - -use super::start_stop::StartStop; -use super::{manager_seed, run_main, ManagerPersistentContainer, RunMainResult}; -use crate::prelude::*; -use crate::procedure::NoOutput; -use crate::s9pk::manifest::Manifest; -use crate::status::MainStatus; -use crate::util::NonDetachingJoinHandle; -use crate::Error; - -pub type ManageContainerOverride = Arc>>; - -pub type Override = MainStatus; - -pub struct OverrideGuard { - override_main_status: Option, -} -impl OverrideGuard { - pub fn drop(self) {} -} -impl Drop for OverrideGuard { - fn drop(&mut self) { - if let Some(override_main_status) = self.override_main_status.take() { - override_main_status.send_modify(|x| { - *x = None; - }); - } - } -} - -/// This is the thing describing the state machine actor for a service -/// state and current running/ desired states. -pub struct ManageContainer { - pub(super) current_state: Arc>, - pub(super) desired_state: Arc>, - _service: NonDetachingJoinHandle<()>, - _save_state: NonDetachingJoinHandle<()>, - override_main_status: ManageContainerOverride, -} - -impl ManageContainer { - pub async fn new( - seed: Arc, - persistent_container: ManagerPersistentContainer, - ) -> Result { - let current_state = Arc::new(watch::channel(StartStop::Stop).0); - let desired_state = Arc::new( - watch::channel::( - get_status(seed.ctx.db.peek().await, &seed.manifest).into(), - ) - .0, - ); - let override_main_status: ManageContainerOverride = Arc::new(watch::channel(None).0); - let service = tokio::spawn(create_service_manager( - desired_state.clone(), - seed.clone(), - current_state.clone(), - persistent_container, - )) - .into(); - let save_state = tokio::spawn(save_state( - desired_state.clone(), - current_state.clone(), - override_main_status.clone(), - seed.clone(), - )) - .into(); - Ok(ManageContainer { - current_state, - desired_state, - _service: service, - override_main_status, - _save_state: save_state, - }) - } - - /// Set override is used during something like a restart of a service. We want to show certain statuses be different - /// from the actual status of the service. - pub fn set_override(&self, override_status: Override) -> Result { - let status = Some(override_status); - if self.override_main_status.borrow().is_some() { - return Err(Error::new( - eyre!("Already have an override"), - ErrorKind::InvalidRequest, - )); - } - self.override_main_status - .send_modify(|x| *x = status.clone()); - Ok(OverrideGuard { - override_main_status: Some(self.override_main_status.clone()), - }) - } - - /// Set the override, but don't have a guard to revert it. Used only on the mananger to do a shutdown. - pub(super) async fn lock_state_forever( - &self, - seed: &manager_seed::ManagerSeed, - ) -> Result<(), Error> { - let current_state = get_status(seed.ctx.db.peek().await, &seed.manifest); - self.override_main_status - .send_modify(|x| *x = Some(current_state)); - Ok(()) - } - - /// We want to set the state of the service, like to start or stop - pub fn to_desired(&self, new_state: StartStop) { - self.desired_state.send_modify(|x| *x = new_state); - } - - /// This is a tool to say wait for the service to be in a certain state. - pub async fn wait_for_desired(&self, new_state: StartStop) { - let mut current_state = self.current_state(); - self.to_desired(new_state); - while *current_state.borrow() != new_state { - current_state.changed().await.unwrap_or_default(); - } - } - - /// Getter - pub fn current_state(&self) -> watch::Receiver { - self.current_state.subscribe() - } - - /// Getter - pub fn desired_state(&self) -> watch::Receiver { - self.desired_state.subscribe() - } -} - -async fn create_service_manager( - desired_state: Arc>, - seed: Arc, - current_state: Arc>, - persistent_container: Arc>, -) { - let mut desired_state_receiver = desired_state.subscribe(); - let mut running_service: Option> = None; - let seed = seed.clone(); - loop { - let current: StartStop = *current_state.borrow(); - let desired: StartStop = *desired_state_receiver.borrow(); - match (current, desired) { - (StartStop::Start, StartStop::Start) => (), - (StartStop::Start, StartStop::Stop) => { - if persistent_container.is_none() { - if let Err(err) = seed.stop_container().await { - tracing::error!("Could not stop container"); - tracing::debug!("{:?}", err) - } - running_service = None; - } else if let Some(current_service) = running_service.take() { - tokio::select! { - _ = current_service => (), - _ = tokio::time::sleep(Duration::from_secs_f64(seed.manifest - .containers - .as_ref() - .and_then(|c| c.main.sigterm_timeout).map(|x| x.as_secs_f64()).unwrap_or_default())) => { - tracing::error!("Could not stop service"); - } - } - } - current_state.send_modify(|x| *x = StartStop::Stop); - } - (StartStop::Stop, StartStop::Start) => starting_service( - current_state.clone(), - desired_state.clone(), - seed.clone(), - persistent_container.clone(), - &mut running_service, - ), - (StartStop::Stop, StartStop::Stop) => (), - } - - if desired_state_receiver.changed().await.is_err() { - tracing::error!("Desired state error"); - break; - } - } -} - -async fn save_state( - desired_state: Arc>, - current_state: Arc>, - override_main_status: ManageContainerOverride, - seed: Arc, -) { - let mut desired_state_receiver = desired_state.subscribe(); - let mut current_state_receiver = current_state.subscribe(); - let mut override_main_status_receiver = override_main_status.subscribe(); - loop { - let current: StartStop = *current_state_receiver.borrow(); - let desired: StartStop = *desired_state_receiver.borrow(); - let override_status = override_main_status_receiver.borrow().clone(); - let status = match (override_status.clone(), current, desired) { - (Some(status), _, _) => status, - (_, StartStop::Start, StartStop::Start) => MainStatus::Running { - started: chrono::Utc::now(), - health: Default::default(), - }, - (_, StartStop::Start, StartStop::Stop) => MainStatus::Stopping, - (_, StartStop::Stop, StartStop::Start) => MainStatus::Starting, - (_, StartStop::Stop, StartStop::Stop) => MainStatus::Stopped, - }; - - let manifest = &seed.manifest; - if let Err(err) = seed - .ctx - .db - .mutate(|db| set_status(db, manifest, &status)) - .await - { - tracing::error!("Did not set status for {}", seed.container_name); - tracing::debug!("{:?}", err); - } - tokio::select! { - _ = desired_state_receiver.changed() =>{}, - _ = current_state_receiver.changed() => {}, - _ = override_main_status_receiver.changed() => {} - } - } -} - -fn starting_service( - current_state: Arc>, - desired_state: Arc>, - seed: Arc, - persistent_container: ManagerPersistentContainer, - running_service: &mut Option>, -) { - let set_running = { - let current_state = current_state.clone(); - Arc::new(move || { - current_state.send_modify(|x| *x = StartStop::Start); - }) - }; - let set_stopped = { move || current_state.send_modify(|x| *x = StartStop::Stop) }; - let running_main_loop = async move { - while desired_state.borrow().is_start() { - let result = run_main( - seed.clone(), - persistent_container.clone(), - set_running.clone(), - ) - .await; - set_stopped(); - run_main_log_result(result, seed.clone()).await; - } - }; - *running_service = Some(tokio::spawn(running_main_loop).into()); -} - -async fn run_main_log_result(result: RunMainResult, seed: Arc) { - match result { - Ok(Ok(NoOutput)) => (), // restart - Ok(Err(e)) => { - tracing::error!( - "The service {} has crashed with the following exit code: {}", - seed.manifest.id.clone(), - e.0 - ); - - tokio::time::sleep(Duration::from_secs(15)).await; - } - Err(e) => { - tracing::error!("failed to start service: {}", e); - tracing::debug!("{:?}", e); - } - } -} - -/// Used only in the mod where we are doing a backup -#[instrument(skip(db, manifest))] -pub(super) fn get_status(db: Peeked, manifest: &Manifest) -> MainStatus { - db.as_package_data() - .as_idx(&manifest.id) - .and_then(|x| x.as_installed()) - .filter(|x| x.as_manifest().as_version().de().ok() == Some(manifest.version.clone())) - .and_then(|x| x.as_status().as_main().de().ok()) - .unwrap_or(MainStatus::Stopped) -} - -#[instrument(skip(db, manifest))] -fn set_status(db: &mut Peeked, manifest: &Manifest, main_status: &MainStatus) -> Result<(), Error> { - let Some(installed) = db - .as_package_data_mut() - .as_idx_mut(&manifest.id) - .or_not_found(&manifest.id)? - .as_installed_mut() - else { - return Ok(()); - }; - installed.as_status_mut().as_main_mut().ser(main_status) -} diff --git a/core/startos/src/manager/manager_map.rs b/core/startos/src/manager/manager_map.rs deleted file mode 100644 index 07f128ccd..000000000 --- a/core/startos/src/manager/manager_map.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; - -use color_eyre::eyre::eyre; -use tokio::sync::RwLock; -use tracing::instrument; - -use super::Manager; -use crate::context::RpcContext; -use crate::prelude::*; -use crate::s9pk::manifest::{Manifest, PackageId}; -use crate::util::Version; -use crate::Error; - -/// This is the structure to contain all the service managers -#[derive(Default)] -pub struct ManagerMap(RwLock>>); -impl ManagerMap { - #[instrument(skip_all)] - pub async fn init(&self, ctx: RpcContext, peeked: Peeked) -> Result<(), Error> { - let mut res = BTreeMap::new(); - for package in peeked.as_package_data().keys()? { - let man: Manifest = if let Some(manifest) = peeked - .as_package_data() - .as_idx(&package) - .and_then(|x| x.as_installed()) - .map(|x| x.as_manifest().de()) - { - manifest? - } else { - continue; - }; - - res.insert( - (package, man.version.clone()), - Arc::new(Manager::new(ctx.clone(), man).await?), - ); - } - *self.0.write().await = res; - Ok(()) - } - - /// Used during the install process - #[instrument(skip_all)] - pub async fn add(&self, ctx: RpcContext, manifest: Manifest) -> Result, Error> { - let mut lock = self.0.write().await; - let id = (manifest.id.clone(), manifest.version.clone()); - if let Some(man) = lock.remove(&id) { - man.exit().await; - } - let manager = Arc::new(Manager::new(ctx.clone(), manifest).await?); - lock.insert(id, manager.clone()); - Ok(manager) - } - - /// This is ran during the cleanup, so when we are uninstalling the service - #[instrument(skip_all)] - pub async fn remove(&self, id: &(PackageId, Version)) { - if let Some(man) = self.0.write().await.remove(id) { - man.exit().await; - } - } - - /// Used during a shutdown - #[instrument(skip_all)] - pub async fn empty(&self) -> Result<(), Error> { - let res = - futures::future::join_all(std::mem::take(&mut *self.0.write().await).into_iter().map( - |((id, version), man)| async move { - tracing::debug!("Manager for {}@{} shutting down", id, version); - man.shutdown().await?; - tracing::debug!("Manager for {}@{} is shutdown", id, version); - if let Err(e) = Arc::try_unwrap(man) { - tracing::trace!( - "Manager for {}@{} still has {} other open references", - id, - version, - Arc::strong_count(&e) - 1 - ); - } - Ok::<_, Error>(()) - }, - )) - .await; - res.into_iter().fold(Ok(()), |res, x| match (res, x) { - (Ok(()), x) => x, - (Err(e), Ok(())) => Err(e), - (Err(e1), Err(e2)) => Err(Error::new(eyre!("{}, {}", e1.source, e2.source), e1.kind)), - }) - } - - #[instrument(skip_all)] - pub async fn get(&self, id: &(PackageId, Version)) -> Option> { - self.0.read().await.get(id).cloned() - } -} diff --git a/core/startos/src/manager/manager_seed.rs b/core/startos/src/manager/manager_seed.rs deleted file mode 100644 index f90e7739f..000000000 --- a/core/startos/src/manager/manager_seed.rs +++ /dev/null @@ -1,37 +0,0 @@ -use models::ErrorKind; - -use crate::context::RpcContext; -use crate::procedure::docker::DockerProcedure; -use crate::procedure::PackageProcedure; -use crate::s9pk::manifest::Manifest; -use crate::util::docker::stop_container; -use crate::Error; - -/// This is helper structure for a service, the seed of the data that is needed for the manager_container -pub struct ManagerSeed { - pub ctx: RpcContext, - pub manifest: Manifest, - pub container_name: String, -} - -impl ManagerSeed { - pub async fn stop_container(&self) -> Result<(), Error> { - match stop_container( - &self.container_name, - match &self.manifest.main { - PackageProcedure::Docker(DockerProcedure { - sigterm_timeout: Some(sigterm_timeout), - .. - }) => Some(**sigterm_timeout), - _ => None, - }, - None, - ) - .await - { - Err(e) if e.kind == ErrorKind::NotFound => (), // Already stopped - a => a?, - } - Ok(()) - } -} diff --git a/core/startos/src/manager/mod.rs b/core/startos/src/manager/mod.rs deleted file mode 100644 index 1151da2f4..000000000 --- a/core/startos/src/manager/mod.rs +++ /dev/null @@ -1,888 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::Ipv4Addr; -use std::sync::Arc; -use std::task::Poll; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use container_init::ProcessGroupId; -use futures::future::BoxFuture; -use futures::{Future, FutureExt, TryFutureExt}; -use helpers::UnixRpcClient; -use models::{ErrorKind, OptionExt, PackageId}; -use nix::sys::signal::Signal; -use persistent_container::PersistentContainer; -use rand::SeedableRng; -use sqlx::Connection; -use start_stop::StartStop; -use tokio::sync::watch::{self, Sender}; -use tokio::sync::{oneshot, Mutex}; -use tracing::instrument; -use transition_state::TransitionState; - -use crate::backup::target::PackageBackupInfo; -use crate::backup::PackageBackupReport; -use crate::config::action::ConfigRes; -use crate::config::spec::ValueSpecPointer; -use crate::config::ConfigureContext; -use crate::context::RpcContext; -use crate::db::model::{CurrentDependencies, CurrentDependencyInfo}; -use crate::dependencies::{ - add_dependent_to_current_dependents_lists, compute_dependency_config_errs, -}; -use crate::disk::mount::backup::BackupMountGuard; -use crate::disk::mount::guard::TmpMountGuard; -use crate::install::cleanup::remove_from_current_dependents_lists; -use crate::net::net_controller::NetService; -use crate::net::vhost::AlpnInfo; -use crate::prelude::*; -use crate::procedure::docker::{DockerContainer, DockerProcedure, LongRunning}; -use crate::procedure::{NoOutput, ProcedureName}; -use crate::s9pk::manifest::Manifest; -use crate::status::MainStatus; -use crate::util::docker::{get_container_ip, kill_container}; -use crate::util::NonDetachingJoinHandle; -use crate::volume::Volume; -use crate::Error; - -pub mod health; -mod manager_container; -mod manager_map; -pub mod manager_seed; -mod persistent_container; -mod start_stop; -mod transition_state; - -pub use manager_map::ManagerMap; - -use self::manager_container::{get_status, ManageContainer}; -use self::manager_seed::ManagerSeed; - -pub const HEALTH_CHECK_COOLDOWN_SECONDS: u64 = 15; -pub const HEALTH_CHECK_GRACE_PERIOD_SECONDS: u64 = 5; - -type ManagerPersistentContainer = Arc>; -type BackupGuard = Arc>>; -pub enum BackupReturn { - Error(Error), - AlreadyRunning(PackageBackupReport), - Ran { - report: PackageBackupReport, - res: Result, - }, -} - -pub struct Gid { - next_gid: (watch::Sender, watch::Receiver), - main_gid: ( - watch::Sender, - watch::Receiver, - ), -} - -impl Default for Gid { - fn default() -> Self { - Self { - next_gid: watch::channel(1), - main_gid: watch::channel(ProcessGroupId(1)), - } - } -} -impl Gid { - pub fn new_gid(&self) -> ProcessGroupId { - let mut previous = 0; - self.next_gid.0.send_modify(|x| { - previous = *x; - *x = previous + 1; - }); - ProcessGroupId(previous) - } - - pub fn new_main_gid(&self) -> ProcessGroupId { - let gid = self.new_gid(); - self.main_gid.0.send(gid).unwrap_or_default(); - gid - } -} - -/// This is the controller of the services. Here is where we can control a service with a start, stop, restart, etc. -#[derive(Clone)] -pub struct Manager { - seed: Arc, - - manage_container: Arc, - transition: Arc>, - persistent_container: ManagerPersistentContainer, - - pub gid: Arc, -} -impl Manager { - pub async fn new(ctx: RpcContext, manifest: Manifest) -> Result { - let seed = Arc::new(ManagerSeed { - ctx, - container_name: DockerProcedure::container_name(&manifest.id, None), - manifest, - }); - - let persistent_container = Arc::new(PersistentContainer::init(&seed).await?); - let manage_container = Arc::new( - manager_container::ManageContainer::new(seed.clone(), persistent_container.clone()) - .await?, - ); - let (transition, _) = watch::channel(Default::default()); - let transition = Arc::new(transition); - Ok(Self { - seed, - manage_container, - transition, - persistent_container, - gid: Default::default(), - }) - } - - /// awaiting this does not wait for the start to complete - pub async fn start(&self) { - if self._is_transition_restart() { - return; - } - self._transition_abort().await; - self.manage_container.to_desired(StartStop::Start); - } - - /// awaiting this does not wait for the stop to complete - pub async fn stop(&self) { - self._transition_abort().await; - self.manage_container.to_desired(StartStop::Stop); - } - /// awaiting this does not wait for the restart to complete - pub async fn restart(&self) { - if self._is_transition_restart() - && *self.manage_container.desired_state().borrow() == StartStop::Stop - { - return; - } - if self.manage_container.desired_state().borrow().is_start() { - self._transition_replace(self._transition_restart()).await; - } - } - /// awaiting this does not wait for the restart to complete - pub async fn configure( - &self, - configure_context: ConfigureContext, - ) -> Result, Error> { - if self._is_transition_restart() { - self._transition_abort().await; - } else if self._is_transition_backup() { - return Err(Error::new( - eyre!("Can't configure because service is backing up"), - ErrorKind::InvalidRequest, - )); - } - let context = self.seed.ctx.clone(); - let id = self.seed.manifest.id.clone(); - - let breakages = configure(context, id, configure_context).await?; - - self.restart().await; - - Ok(breakages) - } - - /// awaiting this does not wait for the backup to complete - pub async fn backup(&self, backup_guard: BackupGuard) -> BackupReturn { - if self._is_transition_backup() { - return BackupReturn::AlreadyRunning(PackageBackupReport { - error: Some("Can't do backup because service is already backing up".to_owned()), - }); - } - let (transition_state, done) = self._transition_backup(backup_guard); - self._transition_replace(transition_state).await; - done.await - } - pub async fn exit(&self) { - self._transition_abort().await; - self.manage_container - .wait_for_desired(StartStop::Stop) - .await; - } - - /// A special exit that is overridden the start state, should only be called in the shutdown, where we remove other containers - async fn shutdown(&self) -> Result<(), Error> { - self.manage_container.lock_state_forever(&self.seed).await?; - - self.exit().await; - Ok(()) - } - - /// Used when we want to shutdown the service - pub async fn signal(&self, signal: Signal) -> Result<(), Error> { - let gid = self.gid.clone(); - send_signal(self, gid, signal).await - } - - /// Used as a getter, but also used in procedure - pub fn rpc_client(&self) -> Option> { - (*self.persistent_container) - .as_ref() - .map(|x| x.rpc_client()) - } - - async fn _transition_abort(&self) { - self.transition - .send_replace(Default::default()) - .abort() - .await; - } - async fn _transition_replace(&self, transition_state: TransitionState) { - self.transition.send_replace(transition_state).abort().await; - } - - pub(super) fn perform_restart(&self) -> impl Future> + 'static { - let manage_container = self.manage_container.clone(); - async move { - let restart_override = manage_container.set_override(MainStatus::Restarting)?; - manage_container.wait_for_desired(StartStop::Stop).await; - manage_container.wait_for_desired(StartStop::Start).await; - restart_override.drop(); - Ok(()) - } - } - fn _transition_restart(&self) -> TransitionState { - let transition = self.transition.clone(); - let restart = self.perform_restart(); - TransitionState::Restarting( - tokio::spawn(async move { - if let Err(err) = restart.await { - tracing::error!("Error restarting service: {}", err); - } - transition.send_replace(Default::default()); - }) - .into(), - ) - } - fn perform_backup( - &self, - backup_guard: BackupGuard, - ) -> impl Future, Error>> { - let manage_container = self.manage_container.clone(); - let seed = self.seed.clone(); - async move { - let peek = seed.ctx.db.peek().await; - let state_reverter = DesiredStateReverter::new(manage_container.clone()); - let override_guard = - manage_container.set_override(get_status(peek, &seed.manifest).backing_up())?; - manage_container.wait_for_desired(StartStop::Stop).await; - let backup_guard = backup_guard.lock().await; - let guard = backup_guard.mount_package_backup(&seed.manifest.id).await?; - - let return_value = seed.manifest.backup.create(seed.clone()).await; - guard.unmount().await?; - drop(backup_guard); - - let manifest_id = seed.manifest.id.clone(); - seed.ctx - .db - .mutate(|db| { - if let Some(progress) = db - .as_server_info_mut() - .as_status_info_mut() - .as_backup_progress_mut() - .transpose_mut() - .and_then(|p| p.as_idx_mut(&manifest_id)) - { - progress.as_complete_mut().ser(&true)?; - } - Ok(()) - }) - .await?; - - state_reverter.revert().await; - - override_guard.drop(); - Ok::<_, Error>(return_value) - } - } - fn _transition_backup( - &self, - backup_guard: BackupGuard, - ) -> (TransitionState, BoxFuture) { - let (send, done) = oneshot::channel(); - - let transition_state = self.transition.clone(); - ( - TransitionState::BackingUp( - tokio::spawn( - self.perform_backup(backup_guard) - .then(finish_up_backup_task(transition_state, send)), - ) - .into(), - ), - done.map_err(|err| Error::new(eyre!("Oneshot error: {err:?}"), ErrorKind::Unknown)) - .map(flatten_backup_error) - .boxed(), - ) - } - fn _is_transition_restart(&self) -> bool { - let transition = self.transition.borrow(); - matches!(*transition, TransitionState::Restarting(_)) - } - fn _is_transition_backup(&self) -> bool { - let transition = self.transition.borrow(); - matches!(*transition, TransitionState::BackingUp(_)) - } -} - -#[instrument(skip_all)] -async fn configure( - ctx: RpcContext, - id: PackageId, - mut configure_context: ConfigureContext, -) -> Result, Error> { - let db = ctx.db.peek().await; - let id = &id; - let ctx = &ctx; - let overrides = &mut configure_context.overrides; - // fetch data from db - let manifest = db - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_manifest() - .de()?; - - // get current config and current spec - let ConfigRes { - config: old_config, - spec, - } = manifest - .config - .as_ref() - .or_not_found("Manifest config")? - .get(ctx, id, &manifest.version, &manifest.volumes) - .await?; - - // determine new config to use - let mut config = if let Some(config) = configure_context.config.or_else(|| old_config.clone()) { - config - } else { - spec.gen( - &mut rand::rngs::StdRng::from_entropy(), - &configure_context.timeout, - )? - }; - - spec.validate(&manifest)?; - spec.matches(&config)?; // check that new config matches spec - - // TODO Commit or not? - spec.update(ctx, &manifest, overrides, &mut config).await?; // dereference pointers in the new config - - let manifest = db - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_installed() - .or_not_found(id)? - .as_manifest() - .de()?; - - let dependencies = &manifest.dependencies; - let mut current_dependencies: CurrentDependencies = CurrentDependencies( - dependencies - .0 - .iter() - .filter_map(|(id, info)| { - if info.requirement.required() { - Some((id.clone(), CurrentDependencyInfo::default())) - } else { - None - } - }) - .collect(), - ); - for ptr in spec.pointers(&config)? { - match ptr { - ValueSpecPointer::Package(pkg_ptr) => { - if let Some(info) = current_dependencies.0.get_mut(pkg_ptr.package_id()) { - info.pointers.insert(pkg_ptr); - } else { - let id = pkg_ptr.package_id().to_owned(); - let mut pointers = BTreeSet::new(); - pointers.insert(pkg_ptr); - current_dependencies.0.insert( - id, - CurrentDependencyInfo { - pointers, - health_checks: BTreeSet::new(), - }, - ); - } - } - ValueSpecPointer::System(_) => (), - } - } - - let action = manifest.config.as_ref().or_not_found(id)?; - let version = &manifest.version; - let volumes = &manifest.volumes; - if !configure_context.dry_run { - // run config action - let res = action - .set(ctx, id, version, &dependencies, volumes, &config) - .await?; - - // track dependencies with no pointers - for (package_id, health_checks) in res.depends_on.into_iter() { - if let Some(current_dependency) = current_dependencies.0.get_mut(&package_id) { - current_dependency.health_checks.extend(health_checks); - } else { - current_dependencies.0.insert( - package_id, - CurrentDependencyInfo { - pointers: BTreeSet::new(), - health_checks, - }, - ); - } - } - - // track dependency health checks - current_dependencies = current_dependencies.map(|x| { - x.into_iter() - .filter(|(dep_id, _)| { - if dep_id != id && !manifest.dependencies.0.contains_key(dep_id) { - tracing::warn!("Illegal dependency specified: {}", dep_id); - false - } else { - true - } - }) - .collect() - }); - } - - let dependency_config_errs = - compute_dependency_config_errs(&ctx, &db, &manifest, ¤t_dependencies, overrides) - .await?; - - // cache current config for dependents - configure_context - .overrides - .insert(id.clone(), config.clone()); - - // handle dependents - - let dependents = db - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_installed() - .or_not_found(id)? - .as_current_dependents() - .de()?; - for (dependent, _dep_info) in dependents.0.iter().filter(|(dep_id, _)| dep_id != &id) { - // check if config passes dependent check - if let Some(cfg) = db - .as_package_data() - .as_idx(dependent) - .or_not_found(dependent)? - .as_installed() - .or_not_found(dependent)? - .as_manifest() - .as_dependencies() - .as_idx(id) - .or_not_found(id)? - .as_config() - .de()? - { - let manifest = db - .as_package_data() - .as_idx(dependent) - .or_not_found(dependent)? - .as_installed() - .or_not_found(dependent)? - .as_manifest() - .de()?; - if let Err(error) = cfg - .check( - ctx, - dependent, - &manifest.version, - &manifest.volumes, - id, - &config, - ) - .await? - { - configure_context.breakages.insert(dependent.clone(), error); - } - } - } - - if !configure_context.dry_run { - return ctx - .db - .mutate(move |db| { - remove_from_current_dependents_lists(db, id, ¤t_dependencies)?; - add_dependent_to_current_dependents_lists(db, id, ¤t_dependencies)?; - current_dependencies.0.remove(id); - for (dep, errs) in db - .as_package_data_mut() - .as_entries_mut()? - .into_iter() - .filter_map(|(id, pde)| { - pde.as_installed_mut() - .map(|i| (id, i.as_status_mut().as_dependency_config_errors_mut())) - }) - { - errs.remove(id)?; - if let Some(err) = configure_context.breakages.get(&dep) { - errs.insert(id, err)?; - } - } - let installed = db - .as_package_data_mut() - .as_idx_mut(id) - .or_not_found(id)? - .as_installed_mut() - .or_not_found(id)?; - installed - .as_current_dependencies_mut() - .ser(¤t_dependencies)?; - let status = installed.as_status_mut(); - status.as_configured_mut().ser(&true)?; - status - .as_dependency_config_errors_mut() - .ser(&dependency_config_errs)?; - Ok(configure_context.breakages) - }) - .await; // add new - } - - Ok(configure_context.breakages) -} - -struct DesiredStateReverter { - manage_container: Option>, - starting_state: StartStop, -} -impl DesiredStateReverter { - fn new(manage_container: Arc) -> Self { - let starting_state = *manage_container.desired_state().borrow(); - let manage_container = Some(manage_container); - Self { - starting_state, - manage_container, - } - } - async fn revert(mut self) { - if let Some(mut current_state) = self._revert() { - while *current_state.borrow() != self.starting_state { - current_state.changed().await.unwrap(); - } - } - } - fn _revert(&mut self) -> Option> { - if let Some(manage_container) = self.manage_container.take() { - manage_container.to_desired(self.starting_state); - - return Some(manage_container.desired_state()); - } - None - } -} -impl Drop for DesiredStateReverter { - fn drop(&mut self) { - self._revert(); - } -} - -type BackupDoneSender = oneshot::Sender>; - -fn finish_up_backup_task( - transition: Arc>, - send: BackupDoneSender, -) -> impl FnOnce(Result, Error>) -> BoxFuture<'static, ()> { - move |result| { - async move { - transition.send_replace(Default::default()); - send.send(match result { - Ok(a) => a, - Err(e) => Err(e), - }) - .unwrap_or_default(); - } - .boxed() - } -} - -fn response_to_report(response: &Result) -> PackageBackupReport { - PackageBackupReport { - error: response.as_ref().err().map(|e| e.to_string()), - } -} -fn flatten_backup_error(input: Result, Error>) -> BackupReturn { - match input { - Ok(a) => BackupReturn::Ran { - report: response_to_report(&a), - res: a, - }, - Err(err) => BackupReturn::Error(err), - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Status { - Starting, - Running, - Stopped, - Paused, - Shutdown, -} - -#[derive(Debug, Clone, Copy)] -pub enum OnStop { - Restart, - Sleep, - Exit, -} - -type RunMainResult = Result, Error>; - -#[instrument(skip_all)] -async fn run_main( - seed: Arc, - persistent_container: ManagerPersistentContainer, - started: Arc, -) -> RunMainResult { - let mut runtime = NonDetachingJoinHandle::from(tokio::spawn(start_up_image(seed.clone()))); - let ip = match persistent_container.is_some() { - false => Some(match get_running_ip(&seed, &mut runtime).await { - GetRunningIp::Ip(x) => x, - GetRunningIp::Error(e) => return Err(e), - GetRunningIp::EarlyExit(x) => return Ok(x), - }), - true => None, - }; - - let svc = if let Some(ip) = ip { - let net = add_network_for_main(&seed, ip).await?; - started(); - Some(net) - } else { - None - }; - - let health = main_health_check_daemon(seed.clone()); - let res = tokio::select! { - a = runtime => a.map_err(|_| Error::new(eyre!("Manager runtime panicked!"), crate::ErrorKind::Docker)).and_then(|a| a), - _ = health => Err(Error::new(eyre!("Health check daemon exited!"), crate::ErrorKind::Unknown)) - }; - if let Some(svc) = svc { - remove_network_for_main(svc).await?; - } - res -} - -/// We want to start up the manifest, but in this case we want to know that we have generated the certificates. -/// Note for _generated_certificate: Needed to know that before we start the state we have generated the certificate -async fn start_up_image(seed: Arc) -> Result, Error> { - seed.manifest - .main - .execute::<(), NoOutput>( - &seed.ctx, - &seed.manifest.id, - &seed.manifest.version, - ProcedureName::Main, - &seed.manifest.volumes, - None, - None, - ) - .await -} - -async fn long_running_docker( - seed: &ManagerSeed, - container: &DockerContainer, -) -> Result<(LongRunning, UnixRpcClient), Error> { - container - .long_running_execute( - &seed.ctx, - &seed.manifest.id, - &seed.manifest.version, - &seed.manifest.volumes, - ) - .await -} - -enum GetRunningIp { - Ip(Ipv4Addr), - Error(Error), - EarlyExit(Result), -} - -async fn get_long_running_ip(seed: &ManagerSeed, runtime: &mut LongRunning) -> GetRunningIp { - loop { - match get_container_ip(&seed.container_name).await { - Ok(Some(ip_addr)) => return GetRunningIp::Ip(ip_addr), - Ok(None) => (), - Err(e) if e.kind == ErrorKind::NotFound => (), - Err(e) => return GetRunningIp::Error(e), - } - if let Poll::Ready(res) = futures::poll!(&mut runtime.running_output) { - match res { - Ok(_) => return GetRunningIp::EarlyExit(Ok(NoOutput)), - Err(_e) => { - return GetRunningIp::Error(Error::new( - eyre!("Manager runtime panicked!"), - crate::ErrorKind::Docker, - )) - } - } - } - } -} - -#[instrument(skip(seed))] -async fn add_network_for_main( - seed: &ManagerSeed, - ip: std::net::Ipv4Addr, -) -> Result { - let mut svc = seed - .ctx - .net_controller - .create_service(seed.manifest.id.clone(), ip) - .await?; - // DEPRECATED - let mut secrets = seed.ctx.secret_store.acquire().await?; - let mut tx = secrets.begin().await?; - for (id, interface) in &seed.manifest.interfaces.0 { - for (external, internal) in interface.lan_config.iter().flatten() { - svc.add_lan( - tx.as_mut(), - id.clone(), - external.0, - internal.internal, - Err(AlpnInfo::Specified(vec![])), - ) - .await?; - } - for (external, internal) in interface.tor_config.iter().flat_map(|t| &t.port_mapping) { - svc.add_tor(tx.as_mut(), id.clone(), external.0, internal.0) - .await?; - } - } - for volume in seed.manifest.volumes.values() { - if let Volume::Certificate { interface_id } = volume { - svc.export_cert(tx.as_mut(), interface_id, ip.into()) - .await?; - } - } - tx.commit().await?; - Ok(svc) -} - -#[instrument(skip(svc))] -async fn remove_network_for_main(svc: NetService) -> Result<(), Error> { - svc.remove_all().await -} - -async fn main_health_check_daemon(seed: Arc) { - tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_GRACE_PERIOD_SECONDS)).await; - loop { - if let Err(e) = health::check(&seed.ctx, &seed.manifest.id).await { - tracing::error!( - "Failed to run health check for {}: {}", - &seed.manifest.id, - e - ); - tracing::debug!("{:?}", e); - } - tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_COOLDOWN_SECONDS)).await; - } -} - -type RuntimeOfCommand = NonDetachingJoinHandle, Error>>; - -#[instrument(skip(seed, runtime))] -async fn get_running_ip(seed: &ManagerSeed, mut runtime: &mut RuntimeOfCommand) -> GetRunningIp { - loop { - match get_container_ip(&seed.container_name).await { - Ok(Some(ip_addr)) => return GetRunningIp::Ip(ip_addr), - Ok(None) => (), - Err(e) if e.kind == ErrorKind::NotFound => (), - Err(e) => return GetRunningIp::Error(e), - } - if let Poll::Ready(res) = futures::poll!(&mut runtime) { - match res { - Ok(Ok(response)) => return GetRunningIp::EarlyExit(response), - Err(e) => { - return GetRunningIp::Error(Error::new( - match e.try_into_panic() { - Ok(e) => { - eyre!( - "Manager runtime panicked: {}", - e.downcast_ref::<&'static str>().unwrap_or(&"UNKNOWN") - ) - } - _ => eyre!("Manager runtime cancelled!"), - }, - crate::ErrorKind::Docker, - )) - } - Ok(Err(e)) => { - return GetRunningIp::Error(Error::new( - eyre!("Manager runtime returned error: {}", e), - crate::ErrorKind::Docker, - )) - } - } - } - } -} - -async fn send_signal(manager: &Manager, gid: Arc, signal: Signal) -> Result<(), Error> { - // stop health checks from committing their results - // shared - // .commit_health_check_results - // .store(false, Ordering::SeqCst); - - if let Some(rpc_client) = manager.rpc_client() { - let main_gid = *gid.main_gid.0.borrow(); - let next_gid = gid.new_gid(); - #[cfg(feature = "js-engine")] - if let Err(e) = crate::procedure::js_scripts::JsProcedure::default() - .execute::<_, NoOutput>( - &manager.seed.ctx.datadir, - &manager.seed.manifest.id, - &manager.seed.manifest.version, - ProcedureName::Signal, - &manager.seed.manifest.volumes, - Some(container_init::SignalGroupParams { - gid: main_gid, - signal: signal as u32, - }), - None, // TODO - next_gid, - Some(rpc_client), - ) - .await? - { - tracing::error!("Failed to send js signal: {}", e.1); - tracing::debug!("{:?}", e); - } - } else { - // send signal to container - kill_container(&manager.seed.container_name, Some(signal)) - .await - .or_else(|e| { - if e.kind == ErrorKind::NotFound { - Ok(()) - } else { - Err(e) - } - })?; - } - - Ok(()) -} diff --git a/core/startos/src/manager/persistent_container.rs b/core/startos/src/manager/persistent_container.rs deleted file mode 100644 index d9868a622..000000000 --- a/core/startos/src/manager/persistent_container.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use helpers::UnixRpcClient; -use tokio::sync::oneshot; -use tokio::sync::watch::{self, Receiver}; -use tracing::instrument; - -use super::manager_seed::ManagerSeed; -use super::{ - add_network_for_main, get_long_running_ip, long_running_docker, remove_network_for_main, - GetRunningIp, -}; -use crate::procedure::docker::DockerContainer; -use crate::util::NonDetachingJoinHandle; -use crate::Error; - -/// Persistant container are the old containers that need to run all the time -/// The goal is that all services will be persistent containers, waiting to run the main system. -pub struct PersistentContainer { - _running_docker: NonDetachingJoinHandle<()>, - pub rpc_client: Receiver>, -} - -impl PersistentContainer { - #[instrument(skip_all)] - pub async fn init(seed: &Arc) -> Result, Error> { - Ok(if let Some(containers) = &seed.manifest.containers { - let (running_docker, rpc_client) = - spawn_persistent_container(seed.clone(), containers.main.clone()).await?; - Some(Self { - _running_docker: running_docker, - rpc_client, - }) - } else { - None - }) - } - - pub fn rpc_client(&self) -> Arc { - self.rpc_client.borrow().clone() - } -} - -pub async fn spawn_persistent_container( - seed: Arc, - container: DockerContainer, -) -> Result<(NonDetachingJoinHandle<()>, Receiver>), Error> { - let (send_inserter, inserter) = oneshot::channel(); - Ok(( - tokio::task::spawn(async move { - let mut inserter_send: Option>> = None; - let mut send_inserter: Option>>> = Some(send_inserter); - loop { - if let Err(e) = async { - let (mut runtime, inserter) = - long_running_docker(&seed, &container).await?; - - - let ip = match get_long_running_ip(&seed, &mut runtime).await { - GetRunningIp::Ip(x) => x, - GetRunningIp::Error(e) => return Err(e), - GetRunningIp::EarlyExit(e) => { - tracing::error!("Early Exit"); - tracing::debug!("{:?}", e); - return Ok(()); - } - }; - let svc = add_network_for_main(&seed, ip).await?; - - if let Some(inserter_send) = inserter_send.as_mut() { - let _ = inserter_send.send(Arc::new(inserter)); - } else { - let (s, r) = watch::channel(Arc::new(inserter)); - inserter_send = Some(s); - if let Some(send_inserter) = send_inserter.take() { - let _ = send_inserter.send(r); - } - } - - let res = tokio::select! { - a = runtime.running_output => a.map_err(|_| Error::new(eyre!("Manager runtime panicked!"), crate::ErrorKind::Docker)).map(|_| ()), - }; - - remove_network_for_main(svc).await?; - - res - }.await { - tracing::error!("Error in persistent container: {}", e); - tracing::debug!("{:?}", e); - } else { - break; - } - tokio::time::sleep(Duration::from_millis(200)).await; - } - }) - .into(), - inserter.await.map_err(|_| Error::new(eyre!("Container handle dropped before inserter sent"), crate::ErrorKind::Unknown))?, - )) -} diff --git a/core/startos/src/manager/start_stop.rs b/core/startos/src/manager/start_stop.rs deleted file mode 100644 index 3842abe57..000000000 --- a/core/startos/src/manager/start_stop.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::status::MainStatus; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum StartStop { - Start, - Stop, -} - -impl StartStop { - pub(crate) fn is_start(&self) -> bool { - matches!(self, StartStop::Start) - } -} -impl From for StartStop { - fn from(value: MainStatus) -> Self { - match value { - MainStatus::Stopped => StartStop::Stop, - MainStatus::Restarting => StartStop::Start, - MainStatus::Stopping => StartStop::Stop, - MainStatus::Starting => StartStop::Start, - MainStatus::Running { - started: _, - health: _, - } => StartStop::Start, - MainStatus::BackingUp { started, health: _ } if started.is_some() => StartStop::Start, - MainStatus::BackingUp { - started: _, - health: _, - } => StartStop::Stop, - } - } -} diff --git a/core/startos/src/manager/transition_state.rs b/core/startos/src/manager/transition_state.rs deleted file mode 100644 index 122c0f703..000000000 --- a/core/startos/src/manager/transition_state.rs +++ /dev/null @@ -1,35 +0,0 @@ -use helpers::NonDetachingJoinHandle; - -/// Used only in the manager/mod and is used to keep track of the state of the manager during the -/// transitional states -pub(super) enum TransitionState { - BackingUp(NonDetachingJoinHandle<()>), - Restarting(NonDetachingJoinHandle<()>), - None, -} - -impl TransitionState { - pub(super) fn take(&mut self) -> Self { - std::mem::take(self) - } - pub(super) fn into_join_handle(self) -> Option> { - Some(match self { - TransitionState::BackingUp(a) => a, - TransitionState::Restarting(a) => a, - TransitionState::None => return None, - }) - } - pub(super) async fn abort(&mut self) { - if let Some(s) = self.take().into_join_handle() { - if s.wait_for_abort().await.is_ok() { - tracing::trace!("transition completed before abort"); - } - } - } -} - -impl Default for TransitionState { - fn default() -> Self { - TransitionState::None - } -} diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index 611923ad6..7c5eaa4c2 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -1,33 +1,38 @@ use std::borrow::Borrow; +use std::collections::BTreeSet; +use std::ops::Deref; use std::sync::Arc; use std::time::{Duration, Instant}; +use axum::extract::Request; +use axum::response::Response; use basic_cookies::Cookie; +use chrono::Utc; use color_eyre::eyre::eyre; use digest::Digest; -use futures::future::BoxFuture; -use futures::FutureExt; -use http::StatusCode; -use rpc_toolkit::command_helpers::prelude::RequestParts; -use rpc_toolkit::hyper::header::COOKIE; -use rpc_toolkit::hyper::http::Error as HttpError; -use rpc_toolkit::hyper::{Body, Request, Response}; -use rpc_toolkit::rpc_server_helpers::{ - noop4, to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, -}; -use rpc_toolkit::yajrc::RpcMethod; -use rpc_toolkit::Metadata; +use helpers::const_true; +use http::header::{COOKIE, USER_AGENT}; +use http::HeaderValue; +use imbl_value::InternedString; +use rpc_toolkit::yajrc::INTERNAL_ERROR; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tokio::sync::Mutex; use crate::context::RpcContext; -use crate::{Error, ResultExt}; +use crate::prelude::*; -pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/embassy/rpc.authcookie"; +pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginRes { + pub session: InternedString, +} pub trait AsLogoutSessionId { - fn as_logout_session_id(self) -> String; + fn as_logout_session_id(self) -> InternedString; } /// Will need to know when we have logged out from a route @@ -36,37 +41,51 @@ pub struct HasLoggedOutSessions(()); impl HasLoggedOutSessions { pub async fn new( - logged_out_sessions: impl IntoIterator, + sessions: impl IntoIterator, ctx: &RpcContext, ) -> Result { - let mut open_authed_websockets = ctx.open_authed_websockets.lock().await; - let mut sqlx_conn = ctx.secret_store.acquire().await?; - for session in logged_out_sessions { - let session = session.as_logout_session_id(); - sqlx::query!( - "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = $1", - session - ) - .execute(sqlx_conn.as_mut()) - .await?; - for socket in open_authed_websockets.remove(&session).unwrap_or_default() { - let _ = socket.send(()); - } + let to_log_out: BTreeSet<_> = sessions + .into_iter() + .map(|s| s.as_logout_session_id()) + .collect(); + for sid in &to_log_out { + ctx.open_authed_continuations.kill(&Some(sid.clone())) } + ctx.ephemeral_sessions.mutate(|s| { + for sid in &to_log_out { + s.0.remove(sid); + } + }); + ctx.db + .mutate(|db| { + let sessions = db.as_private_mut().as_sessions_mut(); + for sid in &to_log_out { + sessions.remove(sid)?; + } + + Ok(()) + }) + .await?; Ok(HasLoggedOutSessions(())) } } /// Used when we need to know that we have logged in with a valid user -#[derive(Clone, Copy)] -pub struct HasValidSession(()); +#[derive(Clone)] +pub struct HasValidSession(SessionType); + +#[derive(Clone)] +enum SessionType { + Local, + Session(HashSessionToken), +} impl HasValidSession { - pub async fn from_request_parts( - request_parts: &RequestParts, + pub async fn from_header( + header: Option<&HeaderValue>, ctx: &RpcContext, ) -> Result { - if let Some(cookie_header) = request_parts.headers.get(COOKIE) { + if let Some(cookie_header) = header { let cookies = Cookie::parse( cookie_header .to_str() @@ -79,7 +98,7 @@ impl HasValidSession { } } if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") { - if let Ok(s) = Self::from_session(&HashSessionToken::from_cookie(cookie), ctx).await + if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await { return Ok(s); } @@ -91,24 +110,41 @@ impl HasValidSession { )) } - pub async fn from_session(session: &HashSessionToken, ctx: &RpcContext) -> Result { - let session_hash = session.hashed(); - let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = $1 AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", session_hash) - .execute(ctx.secret_store.acquire().await?.as_mut()) - .await?; - if session.rows_affected() == 0 { - return Err(Error::new( - eyre!("UNAUTHORIZED"), - crate::ErrorKind::Authorization, - )); + pub async fn from_session( + session_token: HashSessionToken, + ctx: &RpcContext, + ) -> Result { + let session_hash = session_token.hashed(); + if !ctx.ephemeral_sessions.mutate(|s| { + if let Some(session) = s.0.get_mut(session_hash) { + session.last_active = Utc::now(); + true + } else { + false + } + }) { + ctx.db + .mutate(|db| { + db.as_private_mut() + .as_sessions_mut() + .as_idx_mut(session_hash) + .ok_or_else(|| { + Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) + })? + .mutate(|s| { + s.last_active = Utc::now(); + Ok(()) + }) + }) + .await?; } - Ok(Self(())) + Ok(Self(SessionType::Session(session_token))) } pub async fn from_local(local: &Cookie<'_>) -> Result { let token = tokio::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH).await?; if local.get_value() == &*token { - Ok(Self(())) + Ok(Self(SessionType::Local)) } else { Err(Error::new( eyre!("UNAUTHORIZED"), @@ -122,27 +158,31 @@ impl HasValidSession { /// Or when we are using internal valid authenticated service. #[derive(Debug, Clone)] pub struct HashSessionToken { - hashed: String, - token: String, + hashed: InternedString, + token: InternedString, } impl HashSessionToken { pub fn new() -> Self { - let token = base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - &rand::random::<[u8; 16]>(), - ) - .to_lowercase(); - let hashed = Self::hash(&token); + Self::from_token(InternedString::intern( + base32::encode( + base32::Alphabet::Rfc4648 { padding: false }, + &rand::random::<[u8; 16]>(), + ) + .to_lowercase(), + )) + } + + pub fn from_token(token: InternedString) -> Self { + let hashed = Self::hash(&*token); Self { hashed, token } } + pub fn from_cookie(cookie: &Cookie) -> Self { - let token = cookie.get_value().to_owned(); - let hashed = Self::hash(&token); - Self { hashed, token } + Self::from_token(InternedString::intern(cookie.get_value())) } - pub fn from_request_parts(request_parts: &RequestParts) -> Result { - if let Some(cookie_header) = request_parts.headers.get(COOKIE) { + pub fn from_header(header: Option<&HeaderValue>) -> Result { + if let Some(cookie_header) = header { let cookies = Cookie::parse( cookie_header .to_str() @@ -159,33 +199,30 @@ impl HashSessionToken { )) } - pub fn header_value(&self) -> Result { - http::HeaderValue::from_str(&format!( - "session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", - self.token - )) - .with_kind(crate::ErrorKind::Unknown) + pub fn to_login_res(&self) -> LoginRes { + LoginRes { + session: self.token.clone(), + } } - pub fn hashed(&self) -> &str { - self.hashed.as_str() + pub fn hashed(&self) -> &InternedString { + &self.hashed } - pub fn as_hash(self) -> String { - self.hashed - } - fn hash(token: &str) -> String { + fn hash(token: &str) -> InternedString { let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); - base32::encode( - base32::Alphabet::RFC4648 { padding: false }, - hasher.finalize().as_slice(), + InternedString::intern( + base32::encode( + base32::Alphabet::Rfc4648 { padding: false }, + hasher.finalize().as_slice(), + ) + .to_lowercase(), ) - .to_lowercase() } } impl AsLogoutSessionId for HashSessionToken { - fn as_logout_session_id(self) -> String { + fn as_logout_session_id(self) -> InternedString { self.hashed } } @@ -205,80 +242,126 @@ impl Ord for HashSessionToken { self.hashed.cmp(&other.hashed) } } -impl Borrow for HashSessionToken { - fn borrow(&self) -> &String { - &self.hashed +impl Borrow for HashSessionToken { + fn borrow(&self) -> &str { + &*self.hashed } } -pub fn auth(ctx: RpcContext) -> DynMiddleware { - let rate_limiter = Arc::new(Mutex::new((0_usize, Instant::now()))); - Box::new( - move |req: &mut Request, - metadata: M| - -> BoxFuture>, HttpError>> { - let ctx = ctx.clone(); - let rate_limiter = rate_limiter.clone(); - async move { - let mut header_stub = Request::new(Body::empty()); - *header_stub.headers_mut() = req.headers().clone(); - let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| { - async move { - if let Err(e) = HasValidSession::from_request_parts(req, &ctx).await { - if metadata - .get(rpc_req.method.as_str(), "authenticated") - .unwrap_or(true) - { - let (res_parts, _) = Response::new(()).into_parts(); - return Ok(Err(to_response( - &req.headers, - res_parts, - Err(e.into()), - |_| StatusCode::OK, - )?)); - } else if rpc_req.method.as_str() == "auth.login" { - let guard = rate_limiter.lock().await; - if guard.1.elapsed() < Duration::from_secs(20) { - if guard.0 >= 3 { - let (res_parts, _) = Response::new(()).into_parts(); - return Ok(Err(to_response( - &req.headers, - res_parts, - Err(Error::new( - eyre!( - "Please limit login attempts to 3 per 20 seconds." - ), - crate::ErrorKind::RateLimited, - ) - .into()), - |_| StatusCode::OK, - )?)); - } - } - } - } - let m3: DynMiddlewareStage3 = Box::new(move |_, res| { - async move { - let mut guard = rate_limiter.lock().await; - if guard.1.elapsed() < Duration::from_secs(20) { - if res.is_err() { - guard.0 += 1; - } - } else { - guard.0 = 0; - } - guard.1 = Instant::now(); - Ok(Ok(noop4())) - } - .boxed() - }); - Ok(Ok(m3)) - } - .boxed() +#[derive(Deserialize)] +pub struct Metadata { + #[serde(default = "const_true")] + authenticated: bool, + #[serde(default)] + login: bool, + #[serde(default)] + get_session: bool, +} + +#[derive(Clone)] +pub struct Auth { + rate_limiter: Arc>, + cookie: Option, + is_login: bool, + set_cookie: Option, + user_agent: Option, +} +impl Auth { + pub fn new() -> Self { + Self { + rate_limiter: Arc::new(Mutex::new((0, Instant::now()))), + cookie: None, + is_login: false, + set_cookie: None, + user_agent: None, + } + } +} +impl Middleware for Auth { + type Metadata = Metadata; + async fn process_http_request( + &mut self, + _: &RpcContext, + request: &mut Request, + ) -> Result<(), Response> { + self.cookie = request.headers_mut().remove(COOKIE); + self.user_agent = request.headers_mut().remove(USER_AGENT); + Ok(()) + } + async fn process_rpc_request( + &mut self, + context: &RpcContext, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + if metadata.login { + self.is_login = true; + let guard = self.rate_limiter.lock().await; + if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 { + return Err(RpcResponse { + id: request.id.take(), + result: Err(Error::new( + eyre!("Please limit login attempts to 3 per 20 seconds."), + crate::ErrorKind::RateLimited, + ) + .into()), }); - Ok(Ok(m2)) } - .boxed() - }, - ) + if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { + request.params["__auth_userAgent"] = Value::String(Arc::new(user_agent.to_owned())) + // TODO: will this panic? + } + } else if metadata.authenticated { + match HasValidSession::from_header(self.cookie.as_ref(), &context).await { + Err(e) => { + return Err(RpcResponse { + id: request.id.take(), + result: Err(e.into()), + }) + } + Ok(HasValidSession(SessionType::Session(s))) if metadata.get_session => { + request.params["__auth_session"] = + Value::String(Arc::new(s.hashed().deref().to_owned())); + // TODO: will this panic? + } + _ => (), + } + } + Ok(()) + } + async fn process_rpc_response(&mut self, _: &RpcContext, response: &mut RpcResponse) { + if self.is_login { + let mut guard = self.rate_limiter.lock().await; + if guard.1.elapsed() < Duration::from_secs(20) { + if response.result.is_err() { + guard.0 += 1; + } + } else { + guard.0 = 0; + } + guard.1 = Instant::now(); + if response.result.is_ok() { + let res = std::mem::replace(&mut response.result, Err(INTERNAL_ERROR)); + response.result = async { + let res = res?; + let login_res = from_value::(res.clone())?; + self.set_cookie = Some( + HeaderValue::from_str(&format!( + "session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", + login_res.session + )) + .with_kind(crate::ErrorKind::Network)?, + ); + + Ok(res) + } + .await; + } + } + } + async fn process_http_response(&mut self, _: &RpcContext, response: &mut Response) { + if let Some(set_cookie) = self.set_cookie.take() { + response.headers_mut().insert("set-cookie", set_cookie); + } + } } diff --git a/core/startos/src/middleware/cors.rs b/core/startos/src/middleware/cors.rs index 5f33bc08d..60e8247ea 100644 --- a/core/startos/src/middleware/cors.rs +++ b/core/startos/src/middleware/cors.rs @@ -1,61 +1,70 @@ -use futures::FutureExt; -use http::HeaderValue; -use hyper::header::HeaderMap; -use rpc_toolkit::hyper::http::Error as HttpError; -use rpc_toolkit::hyper::{Body, Method, Request, Response}; -use rpc_toolkit::rpc_server_helpers::{ - DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4, -}; -use rpc_toolkit::Metadata; +use axum::body::Body; +use axum::extract::Request; +use axum::response::Response; +use http::{HeaderMap, HeaderValue, Method}; +use rpc_toolkit::{Empty, Middleware}; -fn get_cors_headers(req: &Request) -> HeaderMap { - let mut res = HeaderMap::new(); - if let Some(origin) = req.headers().get("Origin") { - res.insert("Access-Control-Allow-Origin", origin.clone()); - } - if let Some(method) = req.headers().get("Access-Control-Request-Method") { - res.insert("Access-Control-Allow-Methods", method.clone()); +#[derive(Clone)] +pub struct Cors { + headers: HeaderMap, +} +impl Cors { + pub fn new() -> Self { + let mut headers = HeaderMap::new(); + headers.insert( + "Access-Control-Allow-Credentials", + HeaderValue::from_static("true"), + ); + Self { headers } } - if let Some(headers) = req.headers().get("Access-Control-Request-Headers") { - res.insert("Access-Control-Allow-Headers", headers.clone()); + fn get_cors_headers(&mut self, req: &Request) { + if let Some(origin) = req.headers().get("Origin") { + self.headers + .insert("Access-Control-Allow-Origin", origin.clone()); + } else { + self.headers + .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*")); + } + if let Some(method) = req.headers().get("Access-Control-Request-Method") { + self.headers + .insert("Access-Control-Allow-Methods", method.clone()); + } else { + self.headers.insert( + "Access-Control-Allow-Methods", + HeaderValue::from_static("*"), + ); + } + if let Some(headers) = req.headers().get("Access-Control-Request-Headers") { + self.headers + .insert("Access-Control-Allow-Headers", headers.clone()); + } else { + self.headers.insert( + "Access-Control-Allow-Headers", + HeaderValue::from_static("*"), + ); + } } - res.insert( - "Access-Control-Allow-Credentials", - HeaderValue::from_static("true"), - ); - res } - -pub async fn cors( - req: &mut Request, - _metadata: M, -) -> Result>, HttpError> { - let headers = get_cors_headers(req); - if req.method() == Method::OPTIONS { - Ok(Err({ - let mut res = Response::new(Body::empty()); - res.headers_mut().extend(headers.into_iter()); - res - })) - } else { - Ok(Ok(Box::new(|_, _| { - async move { - let res: DynMiddlewareStage3 = Box::new(|_, _| { - async move { - let res: DynMiddlewareStage4 = Box::new(|res| { - async move { - res.headers_mut().extend(headers.into_iter()); - Ok::<_, HttpError>(()) - } - .boxed() - }); - Ok::<_, HttpError>(Ok(res)) - } - .boxed() - }); - Ok::<_, HttpError>(Ok(res)) - } - .boxed() - }))) +impl Middleware for Cors { + type Metadata = Empty; + async fn process_http_request( + &mut self, + _: &Context, + request: &mut Request, + ) -> Result<(), Response> { + self.get_cors_headers(request); + if request.method() == Method::OPTIONS { + let mut response = Response::new(Body::empty()); + response + .headers_mut() + .extend(std::mem::take(&mut self.headers)); + return Err(response); + } + Ok(()) + } + async fn process_http_response(&mut self, _: &Context, response: &mut Response) { + response + .headers_mut() + .extend(std::mem::take(&mut self.headers)) } } diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index c3ceadda6..4e5f0e037 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -1,50 +1,53 @@ -use futures::future::BoxFuture; -use futures::FutureExt; +use axum::response::Response; +use http::header::InvalidHeaderValue; use http::HeaderValue; -use rpc_toolkit::hyper::http::Error as HttpError; -use rpc_toolkit::hyper::{Body, Request, Response}; -use rpc_toolkit::rpc_server_helpers::{ - noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, -}; -use rpc_toolkit::yajrc::RpcMethod; -use rpc_toolkit::Metadata; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; +use serde::Deserialize; use crate::context::RpcContext; -pub fn db(ctx: RpcContext) -> DynMiddleware { - Box::new( - move |_: &mut Request, - metadata: M| - -> BoxFuture>, HttpError>> { - let ctx = ctx.clone(); - async move { - let m2: DynMiddlewareStage2 = Box::new(move |_req, rpc_req| { - async move { - let sync_db = metadata - .get(rpc_req.method.as_str(), "sync_db") - .unwrap_or(false); +#[derive(Deserialize)] +pub struct Metadata { + #[serde(default)] + sync_db: bool, +} + +#[derive(Clone)] +pub struct SyncDb { + sync_db: bool, +} +impl SyncDb { + pub fn new() -> Self { + SyncDb { sync_db: false } + } +} - let m3: DynMiddlewareStage3 = Box::new(move |res, _| { - async move { - if sync_db { - res.headers.append( - "X-Patch-Sequence", - HeaderValue::from_str( - &ctx.db.sequence().await.to_string(), - )?, - ); - } - Ok(Ok(noop4())) - } - .boxed() - }); - Ok(Ok(m3)) - } - .boxed() - }); - Ok(Ok(m2)) +impl Middleware for SyncDb { + type Metadata = Metadata; + async fn process_rpc_request( + &mut self, + _: &RpcContext, + metadata: Self::Metadata, + _: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + self.sync_db = metadata.sync_db; + Ok(()) + } + async fn process_http_response(&mut self, context: &RpcContext, response: &mut Response) { + if let Err(e) = async { + if self.sync_db { + let id = context.db.sequence().await; + response + .headers_mut() + .append("X-Patch-Sequence", HeaderValue::from_str(&id.to_string())?); + context.sync_db.send_replace(id); } - .boxed() - }, - ) + Ok::<_, InvalidHeaderValue>(()) + } + .await + { + tracing::error!("error writing X-Patch-Sequence header: {e}"); + tracing::debug!("{e:?}"); + } + } } diff --git a/core/startos/src/middleware/diagnostic.rs b/core/startos/src/middleware/diagnostic.rs deleted file mode 100644 index 959b8ea2d..000000000 --- a/core/startos/src/middleware/diagnostic.rs +++ /dev/null @@ -1,39 +0,0 @@ -use futures::FutureExt; -use rpc_toolkit::hyper::http::Error as HttpError; -use rpc_toolkit::hyper::{Body, Request, Response}; -use rpc_toolkit::rpc_server_helpers::{noop4, DynMiddlewareStage2, DynMiddlewareStage3}; -use rpc_toolkit::yajrc::RpcMethod; -use rpc_toolkit::Metadata; - -use crate::Error; - -pub async fn diagnostic( - _req: &mut Request, - _metadata: M, -) -> Result>, HttpError> { - Ok(Ok(Box::new(|_, rpc_req| { - let method = rpc_req.method.as_str().to_owned(); - async move { - let res: DynMiddlewareStage3 = Box::new(|_, rpc_res| { - async move { - if let Err(e) = rpc_res { - if e.code == -32601 { - *e = Error::new( - color_eyre::eyre::eyre!( - "{} is not available on the Diagnostic API", - method - ), - crate::ErrorKind::DiagnosticMode, - ) - .into(); - } - } - Ok(Ok(noop4())) - } - .boxed() - }); - Ok::<_, HttpError>(Ok(res)) - } - .boxed() - }))) -} diff --git a/core/startos/src/middleware/encrypt.rs b/core/startos/src/middleware/encrypt.rs deleted file mode 100644 index 94167b7e2..000000000 --- a/core/startos/src/middleware/encrypt.rs +++ /dev/null @@ -1,115 +0,0 @@ -use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher}; -use aes::Aes256Ctr; -use hmac::Hmac; -use josekit::jwk::Jwk; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use tracing::instrument; - -pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey { - let mut aeskey = CipherKey::::default(); - pbkdf2::pbkdf2::>( - password.as_ref(), - salt.as_ref(), - 1000, - aeskey.as_mut_slice(), - ) - .unwrap(); - aeskey -} - -pub fn encrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec { - let prefix: [u8; 32] = rand::random(); - let aeskey = pbkdf2(password.as_ref(), &prefix[16..]); - let ctr = Nonce::::from_slice(&prefix[..16]); - let mut aes = Aes256Ctr::new(&aeskey, ctr); - let mut res = Vec::with_capacity(32 + input.as_ref().len()); - res.extend_from_slice(&prefix[..]); - res.extend_from_slice(input.as_ref()); - aes.apply_keystream(&mut res[32..]); - res -} - -pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec { - if input.as_ref().len() < 32 { - return Vec::new(); - } - let (prefix, rest) = input.as_ref().split_at(32); - let aeskey = pbkdf2(password.as_ref(), &prefix[16..]); - let ctr = Nonce::::from_slice(&prefix[..16]); - let mut aes = Aes256Ctr::new(&aeskey, ctr); - let mut res = rest.to_vec(); - aes.apply_keystream(&mut res); - res -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct EncryptedWire { - encrypted: serde_json::Value, -} -impl EncryptedWire { - #[instrument(skip_all)] - pub fn decrypt(self, current_secret: impl AsRef) -> Option { - let current_secret = current_secret.as_ref(); - - let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs - .decrypter_from_jwk(current_secret) - { - Ok(a) => a, - Err(e) => { - tracing::warn!("Could not setup awk"); - tracing::debug!("{:?}", e); - return None; - } - }; - let encrypted = match serde_json::to_string(&self.encrypted) { - Ok(a) => a, - Err(e) => { - tracing::warn!("Could not deserialize"); - tracing::debug!("{:?}", e); - - return None; - } - }; - let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) { - Ok(a) => a, - Err(e) => { - tracing::warn!("Could not decrypt"); - tracing::debug!("{:?}", e); - return None; - } - }; - match String::from_utf8(decoded) { - Ok(a) => Some(a), - Err(e) => { - tracing::warn!("Could not decrypt into utf8"); - tracing::debug!("{:?}", e); - return None; - } - } - } -} - -/// We created this test by first making the private key, then restoring from this private key for recreatability. -/// After this the frontend then encoded an password, then we are testing that the output that we got (hand coded) -/// will be the shape we want. -#[test] -fn test_gen_awk() { - let private_key: Jwk = serde_json::from_str( - r#"{ - "kty": "EC", - "crv": "P-256", - "d": "3P-MxbUJtEhdGGpBCRFXkUneGgdyz_DGZWfIAGSCHOU", - "x": "yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4", - "y": "8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI" - }"#, - ) - .unwrap(); - let encrypted: EncryptedWire = serde_json::from_str(r#"{ - "encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" } - }"#).unwrap(); - assert_eq!( - "testing12345", - &encrypted.decrypt(std::sync::Arc::new(private_key)).unwrap() - ); -} diff --git a/core/startos/src/middleware/mod.rs b/core/startos/src/middleware/mod.rs index 5af2b8121..3438dc3db 100644 --- a/core/startos/src/middleware/mod.rs +++ b/core/startos/src/middleware/mod.rs @@ -1,5 +1,3 @@ pub mod auth; pub mod cors; pub mod db; -pub mod diagnostic; -pub mod encrypt; diff --git a/core/startos/src/migration.rs b/core/startos/src/migration.rs deleted file mode 100644 index 13f14c7c3..000000000 --- a/core/startos/src/migration.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::BTreeSet; - -use color_eyre::eyre::eyre; -use emver::VersionRange; -use futures::{Future, FutureExt}; -use indexmap::IndexMap; -use models::ImageId; -use patch_db::HasModel; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::context::RpcContext; -use crate::prelude::*; -use crate::procedure::docker::DockerContainers; -use crate::procedure::{PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::PackageId; -use crate::util::Version; -use crate::volume::Volumes; -use crate::{Error, ResultExt}; - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Migrations { - pub from: IndexMap, - pub to: IndexMap, -} -impl Migrations { - #[instrument(skip_all)] - pub fn validate( - &self, - _container: &Option, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - ) -> Result<(), Error> { - for (version, migration) in &self.from { - migration - .validate(eos_version, volumes, image_ids, true) - .with_ctx(|_| { - ( - crate::ErrorKind::ValidateS9pk, - format!("Migration from {}", version), - ) - })?; - } - for (version, migration) in &self.to { - migration - .validate(eos_version, volumes, image_ids, true) - .with_ctx(|_| { - ( - crate::ErrorKind::ValidateS9pk, - format!("Migration to {}", version), - ) - })?; - } - Ok(()) - } - - #[instrument(skip_all)] - pub fn from<'a>( - &'a self, - _container: &'a Option, - ctx: &'a RpcContext, - version: &'a Version, - pkg_id: &'a PackageId, - pkg_version: &'a Version, - volumes: &'a Volumes, - ) -> Option> + 'a> { - if let Some((_, migration)) = self - .from - .iter() - .find(|(range, _)| version.satisfies(*range)) - { - Some(async move { - migration - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::Migration, // Migrations cannot be executed concurrently - volumes, - Some(version), - None, - ) - .map(|r| { - r.and_then(|r| { - r.map_err(|e| { - Error::new(eyre!("{}", e.1), crate::ErrorKind::MigrationFailed) - }) - }) - }) - .await - }) - } else { - None - } - } - - #[instrument(skip_all)] - pub fn to<'a>( - &'a self, - ctx: &'a RpcContext, - version: &'a Version, - pkg_id: &'a PackageId, - pkg_version: &'a Version, - volumes: &'a Volumes, - ) -> Option> + 'a> { - if let Some((_, migration)) = self.to.iter().find(|(range, _)| version.satisfies(*range)) { - Some(async move { - migration - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::Migration, - volumes, - Some(version), - None, - ) - .map(|r| { - r.and_then(|r| { - r.map_err(|e| { - Error::new(eyre!("{}", e.1), crate::ErrorKind::MigrationFailed) - }) - }) - }) - .await - }) - } else { - None - } - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct MigrationRes { - pub configured: bool, -} diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs new file mode 100644 index 000000000..9ef2af1f7 --- /dev/null +++ b/core/startos/src/net/acme.rs @@ -0,0 +1,285 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; + +use async_acme::acme::Identifier; +use clap::builder::ValueParserFactory; +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use models::{ErrorData, FromStrParser}; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::AcmeSettings; +use crate::db::model::Database; +use crate::prelude::*; +use crate::util::serde::{Pem, Pkcs8Doc}; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct AcmeCertStore { + pub accounts: BTreeMap>, Pem>, + pub certs: BTreeMap>, AcmeCert>>, +} +impl AcmeCertStore { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct AcmeCert { + pub key: Pem>, + pub fullchain: Vec>, +} + +pub struct AcmeCertCache<'a>(pub &'a TypedPatchDb); +#[async_trait::async_trait] +impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> { + type Error = ErrorData; + + async fn read_account(&self, contacts: &[&str]) -> Result>, Self::Error> { + let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec()); + let Some(account) = self + .0 + .peek() + .await + .into_private() + .into_key_store() + .into_acme() + .into_accounts() + .into_idx(&contacts) + else { + return Ok(None); + }; + Ok(Some(account.de()?.0.document.into_vec())) + } + + async fn write_account(&self, contacts: &[&str], contents: &[u8]) -> Result<(), Self::Error> { + let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec()); + let key = Pkcs8Doc { + tag: "EC PRIVATE KEY".into(), + document: pkcs8::Document::try_from(contents).with_kind(ErrorKind::Pem)?, + }; + self.0 + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_acme_mut() + .as_accounts_mut() + .insert(&contacts, &Pem::new(key)) + }) + .await?; + Ok(()) + } + + async fn read_certificate( + &self, + identifiers: &[Identifier], + directory_url: &str, + ) -> Result, Self::Error> { + let identifiers = JsonKey::new( + identifiers + .into_iter() + .map(|d| match d { + Identifier::Dns(d) => d.into(), + Identifier::Ip(ip) => InternedString::from_display(ip), + }) + .collect(), + ); + let directory_url = directory_url + .parse::() + .with_kind(ErrorKind::ParseUrl)?; + let Some(cert) = self + .0 + .peek() + .await + .into_private() + .into_key_store() + .into_acme() + .into_certs() + .into_idx(&directory_url) + .and_then(|a| a.into_idx(&identifiers)) + else { + return Ok(None); + }; + let cert = cert.de()?; + Ok(Some(( + String::from_utf8( + cert.key + .0 + .private_key_to_pem_pkcs8() + .with_kind(ErrorKind::OpenSsl)?, + ) + .with_kind(ErrorKind::Utf8)?, + cert.fullchain + .into_iter() + .map(|cert| { + String::from_utf8(cert.0.to_pem().with_kind(ErrorKind::OpenSsl)?) + .with_kind(ErrorKind::Utf8) + }) + .collect::, _>>()? + .join("\n"), + ))) + } + + async fn write_certificate( + &self, + identifiers: &[Identifier], + directory_url: &str, + key_pem: &str, + certificate_pem: &str, + ) -> Result<(), Self::Error> { + tracing::info!("Saving new certificate for {identifiers:?}"); + let identifiers = JsonKey::new( + identifiers + .into_iter() + .map(|d| match d { + Identifier::Dns(d) => d.into(), + Identifier::Ip(ip) => InternedString::from_display(ip), + }) + .collect(), + ); + let directory_url = directory_url + .parse::() + .with_kind(ErrorKind::ParseUrl)?; + let cert = AcmeCert { + key: Pem(PKey::::private_key_from_pem(key_pem.as_bytes()) + .with_kind(ErrorKind::OpenSsl)?), + fullchain: X509::stack_from_pem(certificate_pem.as_bytes()) + .with_kind(ErrorKind::OpenSsl)? + .into_iter() + .map(Pem) + .collect(), + }; + self.0 + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_acme_mut() + .as_certs_mut() + .upsert(&directory_url, || Ok(BTreeMap::new()))? + .insert(&identifiers, &cert) + }) + .await?; + + Ok(()) + } +} + +pub fn acme() -> ParentHandler { + ParentHandler::new() + .subcommand( + "init", + from_fn_async(init) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Setup ACME certificate acquisition") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Setup ACME certificate acquisition") + .with_call_remote::(), + ) +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, TS)] +#[ts(type = "string")] +pub struct AcmeProvider(pub Url); +impl FromStr for AcmeProvider { + type Err = ::Err; + fn from_str(s: &str) -> Result { + match s { + "letsencrypt" => async_acme::acme::LETS_ENCRYPT_PRODUCTION_DIRECTORY.parse(), + "letsencrypt-staging" => async_acme::acme::LETS_ENCRYPT_STAGING_DIRECTORY.parse(), + s => s.parse(), + } + .map(|mut u: Url| { + let path = u + .path_segments() + .into_iter() + .flatten() + .filter(|p| !p.is_empty()) + .map(|p| p.to_owned()) + .collect::>(); + if let Ok(mut path_mut) = u.path_segments_mut() { + path_mut.clear(); + path_mut.extend(path); + } + u + }) + .map(Self) + } +} +impl<'de> Deserialize<'de> for AcmeProvider { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + crate::util::serde::deserialize_from_str(deserializer) + } +} +impl AsRef for AcmeProvider { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} +impl ValueParserFactory for AcmeProvider { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct InitAcmeParams { + #[arg(long)] + pub provider: AcmeProvider, + #[arg(long)] + pub contact: Vec, +} + +pub async fn init( + ctx: RpcContext, + InitAcmeParams { provider, contact }: InitAcmeParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_acme_mut() + .insert(&provider, &AcmeSettings { contact }) + }) + .await?; + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct RemoveAcmeParams { + #[arg(long)] + pub provider: AcmeProvider, +} + +pub async fn remove( + ctx: RpcContext, + RemoveAcmeParams { provider }: RemoveAcmeParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_acme_mut() + .remove(&provider) + }) + .await?; + Ok(()) +} diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs deleted file mode 100644 index cbe7ff19d..000000000 --- a/core/startos/src/net/dhcp.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::IpAddr; - -use futures::TryStreamExt; -use rpc_toolkit::command; -use tokio::sync::RwLock; - -use crate::context::RpcContext; -use crate::db::model::IpInfo; -use crate::net::utils::{iface_is_physical, list_interfaces}; -use crate::prelude::*; -use crate::util::display_none; -use crate::Error; - -lazy_static::lazy_static! { - static ref CACHED_IPS: RwLock> = RwLock::new(BTreeSet::new()); -} - -async fn _ips() -> Result, Error> { - Ok(init_ips() - .await? - .values() - .flat_map(|i| { - std::iter::empty() - .chain(i.ipv4.map(IpAddr::from)) - .chain(i.ipv6.map(IpAddr::from)) - }) - .collect()) -} - -pub async fn ips() -> Result, Error> { - let ips = CACHED_IPS.read().await.clone(); - if !ips.is_empty() { - return Ok(ips); - } - let ips = _ips().await?; - *CACHED_IPS.write().await = ips.clone(); - Ok(ips) -} - -pub async fn init_ips() -> Result, Error> { - let mut res = BTreeMap::new(); - let mut ifaces = list_interfaces(); - while let Some(iface) = ifaces.try_next().await? { - if iface_is_physical(&iface).await { - let ip_info = IpInfo::for_interface(&iface).await?; - res.insert(iface, ip_info); - } - } - Ok(res) -} - -#[command(subcommands(update))] -pub async fn dhcp() -> Result<(), Error> { - Ok(()) -} - -#[command(display(display_none))] -pub async fn update(#[context] ctx: RpcContext, #[arg] interface: String) -> Result<(), Error> { - if iface_is_physical(&interface).await { - let ip_info = IpInfo::for_interface(&interface).await?; - ctx.db - .mutate(|db| { - db.as_server_info_mut() - .as_ip_info_mut() - .insert(&interface, &ip_info) - }) - .await?; - - let mut cached = CACHED_IPS.write().await; - if cached.is_empty() { - *cached = _ips().await?; - } else { - cached.extend( - std::iter::empty() - .chain(ip_info.ipv4.map(IpAddr::from)) - .chain(ip_info.ipv6.map(IpAddr::from)), - ); - } - } - Ok(()) -} diff --git a/core/startos/src/net/dns.rs b/core/startos/src/net/dns.rs index 7b2784a50..d68a971f1 100644 --- a/core/startos/src/net/dns.rs +++ b/core/startos/src/net/dns.rs @@ -1,6 +1,6 @@ use std::borrow::Borrow; use std::collections::BTreeMap; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::Ipv4Addr; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -18,6 +18,8 @@ use trust_dns_server::proto::rr::{Name, Record, RecordType}; use trust_dns_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; use trust_dns_server::ServerFuture; +use crate::net::forward::START9_BRIDGE_IFACE; +use crate::util::sync::Watch; use crate::util::Invoke; use crate::{Error, ErrorKind, ResultExt}; @@ -33,7 +35,7 @@ struct Resolver { impl Resolver { async fn resolve(&self, name: &Name) -> Option> { match name.iter().next_back() { - Some(b"embassy") => { + Some(b"embassy") | Some(b"startos") => { if let Some(pkg) = name.iter().rev().skip(1).next() { if let Some(ip) = self.services.read().await.get(&Some( std::str::from_utf8(pkg) @@ -97,16 +99,8 @@ impl RequestHandler for Resolver { ) .await } - a => { - if a != RecordType::AAAA { - tracing::warn!( - "Non A-Record requested for {}: {:?}", - query.name(), - query.query_type() - ); - } - let mut res = Header::response_from_request(request.header()); - res.set_response_code(ResponseCode::NXDomain); + _ => { + let res = Header::response_from_request(request.header()); response_handle .send_response( MessageResponseBuilder::from_message_request(&*request).build( @@ -147,38 +141,46 @@ impl RequestHandler for Resolver { impl DnsController { #[instrument(skip_all)] - pub async fn init(bind: &[SocketAddr]) -> Result { + pub async fn init(mut lxcbr_status: Watch) -> Result { let services = Arc::new(RwLock::new(BTreeMap::new())); let mut server = ServerFuture::new(Resolver { services: services.clone(), }); - server.register_listener( - TcpListener::bind(bind) - .await - .with_kind(ErrorKind::Network)?, - Duration::from_secs(30), - ); - server.register_socket(UdpSocket::bind(bind).await.with_kind(ErrorKind::Network)?); - Command::new("resolvectl") - .arg("dns") - .arg("br-start9") - .arg("127.0.0.1") - .invoke(ErrorKind::Network) - .await?; - Command::new("resolvectl") - .arg("domain") - .arg("br-start9") - .arg("embassy") - .invoke(ErrorKind::Network) - .await?; + let dns_server = tokio::spawn(async move { + server.register_listener( + TcpListener::bind((Ipv4Addr::LOCALHOST, 53)) + .await + .with_kind(ErrorKind::Network)?, + Duration::from_secs(30), + ); + server.register_socket( + UdpSocket::bind((Ipv4Addr::LOCALHOST, 53)) + .await + .with_kind(ErrorKind::Network)?, + ); + + lxcbr_status.wait_for(|a| *a).await; + + Command::new("resolvectl") + .arg("dns") + .arg(START9_BRIDGE_IFACE) + .arg("127.0.0.1") + .invoke(ErrorKind::Network) + .await?; + Command::new("resolvectl") + .arg("domain") + .arg(START9_BRIDGE_IFACE) + .arg("embassy") + .invoke(ErrorKind::Network) + .await?; - let dns_server = tokio::spawn( server .block_until_done() - .map_err(|e| Error::new(e, ErrorKind::Network)), - ) + .await + .map_err(|e| Error::new(e, ErrorKind::Network)) + }) .into(); Ok(Self { diff --git a/core/startos/src/net/forward.rs b/core/startos/src/net/forward.rs new file mode 100644 index 000000000..6f3abee15 --- /dev/null +++ b/core/startos/src/net/forward.rs @@ -0,0 +1,307 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::net::SocketAddr; +use std::sync::{Arc, Weak}; + +use futures::channel::oneshot; +use helpers::NonDetachingJoinHandle; +use id_pool::IdPool; +use imbl_value::InternedString; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; +use tokio::sync::mpsc; + +use crate::db::model::public::NetworkInterfaceInfo; +use crate::prelude::*; +use crate::util::sync::Watch; +use crate::util::Invoke; + +pub const START9_BRIDGE_IFACE: &str = "lxcbr0"; +pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152; + +#[derive(Debug, Deserialize, Serialize)] +pub struct AvailablePorts(IdPool); +impl AvailablePorts { + pub fn new() -> Self { + Self(IdPool::new_ranged(FIRST_DYNAMIC_PRIVATE_PORT..u16::MAX)) + } + pub fn alloc(&mut self) -> Result { + self.0.request_id().ok_or_else(|| { + Error::new( + eyre!("No more dynamic ports available!"), + ErrorKind::Network, + ) + }) + } + pub fn free(&mut self, ports: impl IntoIterator) { + for port in ports { + self.0.return_id(port).unwrap_or_default(); + } + } +} + +#[derive(Debug)] +struct ForwardRequest { + public: bool, + target: SocketAddr, + rc: Weak<()>, +} + +#[derive(Debug, Default)] +struct ForwardState { + requested: BTreeMap, + current: BTreeMap>, +} +impl ForwardState { + async fn sync(&mut self, interfaces: &BTreeMap) -> Result<(), Error> { + let private_interfaces = interfaces + .iter() + .filter(|(_, public)| !*public) + .map(|(i, _)| i) + .collect::>(); + let all_interfaces = interfaces.keys().collect::>(); + self.requested.retain(|_, req| req.rc.strong_count() > 0); + for external in self + .requested + .keys() + .chain(self.current.keys()) + .copied() + .collect::>() + { + match ( + self.requested.get(&external), + self.current.get_mut(&external), + ) { + (Some(req), Some(cur)) => { + let expected = if req.public { + &all_interfaces + } else { + &private_interfaces + }; + let actual = cur.keys().collect::>(); + let mut to_rm = actual + .difference(expected) + .copied() + .cloned() + .collect::>(); + let mut to_add = expected + .difference(&actual) + .copied() + .cloned() + .collect::>(); + for interface in actual.intersection(expected).copied() { + if cur[interface] != req.target { + to_rm.insert(interface.clone()); + to_add.insert(interface.clone()); + } + } + for interface in to_rm { + unforward(external, &*interface, cur[&interface]).await?; + cur.remove(&interface); + } + for interface in to_add { + forward(external, &*interface, req.target).await?; + cur.insert(interface, req.target); + } + } + (Some(req), None) => { + let cur = self.current.entry(external).or_default(); + for interface in if req.public { + &all_interfaces + } else { + &private_interfaces + } + .into_iter() + .copied() + .cloned() + { + forward(external, &*interface, req.target).await?; + cur.insert(interface, req.target); + } + } + (None, Some(cur)) => { + let to_rm = cur.keys().cloned().collect::>(); + for interface in to_rm { + unforward(external, &*interface, cur[&interface]).await?; + cur.remove(&interface); + } + self.current.remove(&external); + } + _ => (), + } + } + Ok(()) + } +} + +fn err_has_exited(_: T) -> Error { + Error::new( + eyre!("PortForwardController thread has exited"), + ErrorKind::Unknown, + ) +} + +pub struct LanPortForwardController { + req: mpsc::UnboundedSender<( + Option<(u16, ForwardRequest)>, + oneshot::Sender>, + )>, + _thread: NonDetachingJoinHandle<()>, +} +impl LanPortForwardController { + pub fn new(mut ip_info: Watch>) -> Self { + let (req_send, mut req_recv) = mpsc::unbounded_channel(); + let thread = NonDetachingJoinHandle::from(tokio::spawn(async move { + let mut state = ForwardState::default(); + let mut interfaces = ip_info.peek_and_mark_seen(|ip_info| { + ip_info + .iter() + .map(|(iface, info)| (iface.clone(), info.public())) + .collect() + }); + let mut reply: Option>> = None; + loop { + tokio::select! { + msg = req_recv.recv() => { + if let Some((msg, re)) = msg { + if let Some((external, req)) = msg { + state.requested.insert(external, req); + } + reply = Some(re); + } else { + break; + } + } + _ = ip_info.changed() => { + interfaces = ip_info.peek(|ip_info| { + ip_info + .iter() + .map(|(iface, info)| (iface.clone(), info.public())) + .collect() + }); + } + } + let res = state.sync(&interfaces).await; + if let Err(e) = &res { + tracing::error!("Error in PortForwardController: {e}"); + tracing::debug!("{e:?}"); + } + if let Some(re) = reply.take() { + let _ = re.send(res); + } + } + })); + Self { + req: req_send, + _thread: thread, + } + } + pub async fn add(&self, port: u16, public: bool, target: SocketAddr) -> Result, Error> { + let rc = Arc::new(()); + let (send, recv) = oneshot::channel(); + self.req + .send(( + Some(( + port, + ForwardRequest { + public, + target, + rc: Arc::downgrade(&rc), + }, + )), + send, + )) + .map_err(err_has_exited)?; + + recv.await.map_err(err_has_exited)?.map(|_| rc) + } + pub async fn gc(&self) -> Result<(), Error> { + let (send, recv) = oneshot::channel(); + self.req.send((None, send)).map_err(err_has_exited)?; + + recv.await.map_err(err_has_exited)? + } +} + +// iptables -I FORWARD -o br-start9 -p tcp -d 172.18.0.2 --dport 8333 -j ACCEPT +// iptables -t nat -I PREROUTING -p tcp --dport 32768 -j DNAT --to 172.18.0.2:8333 +async fn forward(external: u16, interface: &str, target: SocketAddr) -> Result<(), Error> { + for proto in ["tcp", "udp"] { + Command::new("iptables") + .arg("-I") + .arg("FORWARD") + .arg("-i") + .arg(interface) + .arg("-o") + .arg(START9_BRIDGE_IFACE) + .arg("-p") + .arg(proto) + .arg("-d") + .arg(target.ip().to_string()) + .arg("--dport") + .arg(target.port().to_string()) + .arg("-j") + .arg("ACCEPT") + .invoke(crate::ErrorKind::Network) + .await?; + Command::new("iptables") + .arg("-t") + .arg("nat") + .arg("-I") + .arg("PREROUTING") + .arg("-i") + .arg(interface) + .arg("-p") + .arg(proto) + .arg("--dport") + .arg(external.to_string()) + .arg("-j") + .arg("DNAT") + .arg("--to") + .arg(target.to_string()) + .invoke(crate::ErrorKind::Network) + .await?; + } + Ok(()) +} + +// iptables -D FORWARD -o br-start9 -p tcp -d 172.18.0.2 --dport 8333 -j ACCEPT +// iptables -t nat -D PREROUTING -p tcp --dport 32768 -j DNAT --to 172.18.0.2:8333 +async fn unforward(external: u16, interface: &str, target: SocketAddr) -> Result<(), Error> { + for proto in ["tcp", "udp"] { + Command::new("iptables") + .arg("-D") + .arg("FORWARD") + .arg("-i") + .arg(interface) + .arg("-o") + .arg(START9_BRIDGE_IFACE) + .arg("-p") + .arg(proto) + .arg("-d") + .arg(target.ip().to_string()) + .arg("--dport") + .arg(target.port().to_string()) + .arg("-j") + .arg("ACCEPT") + .invoke(crate::ErrorKind::Network) + .await?; + Command::new("iptables") + .arg("-t") + .arg("nat") + .arg("-D") + .arg("PREROUTING") + .arg("-i") + .arg(interface) + .arg("-p") + .arg(proto) + .arg("--dport") + .arg(external.to_string()) + .arg("-j") + .arg("DNAT") + .arg("--to") + .arg(target.to_string()) + .invoke(crate::ErrorKind::Network) + .await?; + } + Ok(()) +} diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs new file mode 100644 index 000000000..b75734a13 --- /dev/null +++ b/core/startos/src/net/host/address.rs @@ -0,0 +1,304 @@ +use std::collections::BTreeSet; + +use clap::Parser; +use imbl_value::InternedString; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use torut::onion::OnionAddressV3; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::DatabaseModel; +use crate::net::acme::AcmeProvider; +use crate::net::host::{all_hosts, HostApiKind}; +use crate::prelude::*; +use crate::util::serde::{display_serializable, HandlerExtSerde}; + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[serde(rename_all_fields = "camelCase")] +#[serde(tag = "kind")] +#[ts(export)] +pub enum HostAddress { + Onion { + #[ts(type = "string")] + address: OnionAddressV3, + }, + Domain { + #[ts(type = "string")] + address: InternedString, + public: bool, + acme: Option, + }, +} + +#[derive(Debug, Deserialize, Serialize, TS)] +pub struct DomainConfig { + pub public: bool, + pub acme: Option, +} + +fn check_duplicates(db: &DatabaseModel) -> Result<(), Error> { + let mut onions = BTreeSet::::new(); + let mut domains = BTreeSet::::new(); + let mut check_onion = |onion: OnionAddressV3| { + if onions.contains(&onion) { + return Err(Error::new( + eyre!("onion address {onion} is already in use"), + ErrorKind::InvalidRequest, + )); + } + onions.insert(onion); + Ok(()) + }; + let mut check_domain = |domain: InternedString| { + if domains.contains(&domain) { + return Err(Error::new( + eyre!("domain {domain} is already in use"), + ErrorKind::InvalidRequest, + )); + } + domains.insert(domain); + Ok(()) + }; + for host in all_hosts(db) { + let host = host?; + for onion in host.as_onions().de()? { + check_onion(onion)?; + } + for domain in host.as_domains().keys()? { + check_domain(domain)?; + } + } + Ok(()) +} + +pub fn address_api( +) -> ParentHandler { + ParentHandler::::new() + .subcommand( + "domain", + ParentHandler::::new() + .subcommand( + "add", + from_fn_async(add_domain::) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|_, a| a) + .no_display() + .with_about("Add an address to this host") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_domain::) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|_, a| a) + .no_display() + .with_about("Remove an address from this host") + .with_call_remote::(), + ) + .with_inherited(Kind::inheritance), + ) + .subcommand( + "onion", + ParentHandler::::new() + .subcommand( + "add", + from_fn_async(add_onion::) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|_, a| a) + .no_display() + .with_about("Add an address to this host") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_onion::) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|_, a| a) + .no_display() + .with_about("Remove an address from this host") + .with_call_remote::(), + ) + .with_inherited(Kind::inheritance), + ) + .subcommand( + "list", + from_fn_async(list_addresses::) + .with_inherited(Kind::inheritance) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + display_serializable(format, res); + return Ok(()); + } + + let mut table = Table::new(); + table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); + for address in &res { + match address { + HostAddress::Onion { address } => { + table.add_row(row![address, true, "N/A"]); + } + HostAddress::Domain { + address, + public, + acme, + } => { + table.add_row(row![ + address, + *public, + acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") + ]); + } + } + } + + table.print_tty(false)?; + + Ok(()) + }) + .with_about("List addresses for this host") + .with_call_remote::(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddDomainParams { + pub domain: InternedString, + #[arg(long)] + pub private: bool, + #[arg(long)] + pub acme: Option, +} + +pub async fn add_domain( + ctx: RpcContext, + AddDomainParams { + domain, + private, + acme, + }: AddDomainParams, + inheritance: Kind::Inheritance, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if let Some(acme) = &acme { + if !db.as_public().as_server_info().as_acme().contains_key(&acme)? { + return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest)); + } + } + + Kind::host_for(&inheritance, db)? + .as_domains_mut() + .insert( + &domain, + &DomainConfig { + public: !private, + acme, + }, + )?; + check_duplicates(db) + }) + .await?; + Kind::sync_host(&ctx, inheritance).await?; + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct RemoveDomainParams { + pub domain: InternedString, +} + +pub async fn remove_domain( + ctx: RpcContext, + RemoveDomainParams { domain }: RemoveDomainParams, + inheritance: Kind::Inheritance, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + Kind::host_for(&inheritance, db)? + .as_domains_mut() + .remove(&domain) + }) + .await?; + Kind::sync_host(&ctx, inheritance).await?; + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct OnionParams { + pub onion: String, +} + +pub async fn add_onion( + ctx: RpcContext, + OnionParams { onion }: OnionParams, + inheritance: Kind::Inheritance, +) -> Result<(), Error> { + let onion = onion + .strip_suffix(".onion") + .ok_or_else(|| { + Error::new( + eyre!("onion hostname must end in .onion"), + ErrorKind::InvalidOnionAddress, + ) + })? + .parse::()?; + ctx.db + .mutate(|db| { + db.as_private().as_key_store().as_onion().get_key(&onion)?; + + Kind::host_for(&inheritance, db)? + .as_onions_mut() + .mutate(|a| Ok(a.insert(onion)))?; + check_duplicates(db) + }) + .await?; + + Kind::sync_host(&ctx, inheritance).await?; + + Ok(()) +} + +pub async fn remove_onion( + ctx: RpcContext, + OnionParams { onion }: OnionParams, + inheritance: Kind::Inheritance, +) -> Result<(), Error> { + let onion = onion + .strip_suffix(".onion") + .ok_or_else(|| { + Error::new( + eyre!("onion hostname must end in .onion"), + ErrorKind::InvalidOnionAddress, + ) + })? + .parse::()?; + ctx.db + .mutate(|db| { + Kind::host_for(&inheritance, db)? + .as_onions_mut() + .mutate(|a| Ok(a.remove(&onion))) + }) + .await?; + + Kind::sync_host(&ctx, inheritance).await?; + + Ok(()) +} + +pub async fn list_addresses( + ctx: RpcContext, + _: Empty, + inheritance: Kind::Inheritance, +) -> Result, Error> { + Ok(Kind::host_for(&inheritance, &mut ctx.db.peek().await)? + .de()? + .addresses() + .collect()) +} diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs new file mode 100644 index 000000000..5b726a82d --- /dev/null +++ b/core/startos/src/net/host/binding.rs @@ -0,0 +1,244 @@ +use std::collections::BTreeMap; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use clap::Parser; +use models::{FromStrParser, HostId}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::net::forward::AvailablePorts; +use crate::net::host::HostApiKind; +use crate::net::vhost::AlpnInfo; +use crate::prelude::*; +use crate::util::serde::{display_serializable, HandlerExtSerde}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct BindId { + pub id: HostId, + pub internal_port: u16, +} +impl ValueParserFactory for BindId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} +impl FromStr for BindId { + type Err = Error; + fn from_str(s: &str) -> Result { + let (id, port) = s + .split_once(":") + .ok_or_else(|| Error::new(eyre!("expected :"), ErrorKind::ParseUrl))?; + Ok(Self { + id: id.parse()?, + internal_port: port.parse()?, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindInfo { + pub enabled: bool, + pub options: BindOptions, + pub net: NetInfo, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct NetInfo { + pub public: bool, + pub assigned_port: Option, + pub assigned_ssl_port: Option, +} +impl BindInfo { + pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result { + let mut assigned_port = None; + let mut assigned_ssl_port = None; + if options.secure.is_some() { + assigned_port = Some(available_ports.alloc()?); + } + if options.add_ssl.is_some() { + assigned_ssl_port = Some(available_ports.alloc()?); + } + Ok(Self { + enabled: true, + options, + net: NetInfo { + public: false, + assigned_port, + assigned_ssl_port, + }, + }) + } + pub fn update( + self, + available_ports: &mut AvailablePorts, + options: BindOptions, + ) -> Result { + let Self { net: mut lan, .. } = self; + if options + .secure + .map_or(false, |s| !(s.ssl && options.add_ssl.is_some())) + // doesn't make sense to have 2 listening ports, both with ssl + { + lan.assigned_port = if let Some(port) = lan.assigned_port.take() { + Some(port) + } else { + Some(available_ports.alloc()?) + }; + } else { + if let Some(port) = lan.assigned_port.take() { + available_ports.free([port]); + } + } + if options.add_ssl.is_some() { + lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() { + Some(port) + } else { + Some(available_ports.alloc()?) + }; + } else { + if let Some(port) = lan.assigned_ssl_port.take() { + available_ports.free([port]); + } + } + Ok(Self { + enabled: true, + options, + net: lan, + }) + } + pub fn disable(&mut self) { + self.enabled = false; + } +} + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct Security { + pub ssl: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindOptions { + pub preferred_external_port: u16, + pub add_ssl: Option, + pub secure: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddSslOptions { + pub preferred_external_port: u16, + // #[serde(default)] + // pub add_x_forwarded_headers: bool, // TODO + pub alpn: Option, +} + +pub fn binding( +) -> ParentHandler { + ParentHandler::::new() + .subcommand( + "list", + from_fn_async(list_bindings::) + .with_inherited(Kind::inheritance) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + return Ok(display_serializable(format, res)); + } + + let mut table = Table::new(); + table.add_row(row![bc => "INTERNAL PORT", "ENABLED", "PUBLIC", "EXTERNAL PORT", "EXTERNAL SSL PORT"]); + for (internal, info) in res { + table.add_row(row![ + internal, + info.enabled, + info.net.public, + if let Some(port) = info.net.assigned_port { + port.to_string() + } else { + "N/A".to_owned() + }, + if let Some(port) = info.net.assigned_ssl_port { + port.to_string() + } else { + "N/A".to_owned() + }, + ]); + } + + table.print_tty(false).unwrap(); + + Ok(()) + }) + .with_about("List bindinges for this host") + .with_call_remote::(), + ) + .subcommand( + "set-public", + from_fn_async(set_public::) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(Kind::inheritance) + .no_display() + .with_about("Add an binding to this host") + .with_call_remote::(), + ) +} + +pub async fn list_bindings( + ctx: RpcContext, + _: Empty, + inheritance: Kind::Inheritance, +) -> Result, Error> { + Kind::host_for(&inheritance, &mut ctx.db.peek().await)? + .as_bindings() + .de() +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindingSetPublicParams { + internal_port: u16, + #[arg(long)] + public: Option, +} + +pub async fn set_public( + ctx: RpcContext, + BindingSetPublicParams { + internal_port, + public, + }: BindingSetPublicParams, + inheritance: Kind::Inheritance, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + Kind::host_for(&inheritance, db)? + .as_bindings_mut() + .mutate(|b| { + b.get_mut(&internal_port) + .or_not_found(internal_port)? + .net + .public = public.unwrap_or(true); + Ok(()) + }) + }) + .await?; + Kind::sync_host(&ctx, inheritance).await +} diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs new file mode 100644 index 000000000..df209cef5 --- /dev/null +++ b/core/startos/src/net/host/mod.rs @@ -0,0 +1,268 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; +use std::panic::RefUnwindSafe; + +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use models::{HostId, PackageId}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, OrEmpty, ParentHandler}; +use serde::{Deserialize, Serialize}; +use torut::onion::OnionAddressV3; +use ts_rs::TS; + +use crate::context::RpcContext; +use crate::db::model::DatabaseModel; +use crate::net::forward::AvailablePorts; +use crate::net::host::address::{address_api, DomainConfig, HostAddress}; +use crate::net::host::binding::{binding, BindInfo, BindOptions}; +use crate::net::service_interface::HostnameInfo; +use crate::prelude::*; + +pub mod address; +pub mod binding; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Host { + pub bindings: BTreeMap, + #[ts(type = "string[]")] + pub onions: BTreeSet, + #[ts(as = "BTreeMap::")] + pub domains: BTreeMap, + /// COMPUTED: NetService::update + pub hostname_info: BTreeMap>, // internal port -> Hostnames +} +impl AsRef for Host { + fn as_ref(&self) -> &Host { + self + } +} +impl Host { + pub fn new() -> Self { + Self::default() + } + pub fn addresses<'a>(&'a self) -> impl Iterator + 'a { + self.onions + .iter() + .cloned() + .map(|address| HostAddress::Onion { address }) + .chain( + self.domains + .iter() + .map( + |(address, DomainConfig { public, acme })| HostAddress::Domain { + address: address.clone(), + public: *public, + acme: acme.clone(), + }, + ), + ) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[model = "Model"] +#[ts(export)] +pub struct Hosts(pub BTreeMap); + +impl Map for Hosts { + type Key = HostId; + type Value = Host; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +pub fn host_for<'a>( + db: &'a mut DatabaseModel, + package_id: Option<&PackageId>, + host_id: &HostId, +) -> Result<&'a mut Model, Error> { + let Some(package_id) = package_id else { + return Ok(db.as_public_mut().as_server_info_mut().as_host_mut()); + }; + fn host_info<'a>( + db: &'a mut DatabaseModel, + package_id: &PackageId, + ) -> Result<&'a mut Model, Error> { + Ok::<_, Error>( + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_hosts_mut(), + ) + } + let tor_key = if host_info(db, package_id)?.as_idx(host_id).is_none() { + Some( + db.as_private_mut() + .as_key_store_mut() + .as_onion_mut() + .new_key()?, + ) + } else { + None + }; + host_info(db, package_id)?.upsert(host_id, || { + let mut h = Host::new(); + h.onions.insert( + tor_key + .or_not_found("generated tor key")? + .public() + .get_onion_address(), + ); + Ok(h) + }) +} + +pub fn all_hosts(db: &DatabaseModel) -> impl Iterator, Error>> { + [Ok(db.as_public().as_server_info().as_host())] + .into_iter() + .chain( + [db.as_public().as_package_data().as_entries()] + .into_iter() + .flatten_ok() + .map(|entry| entry.and_then(|(_, v)| v.as_hosts().as_entries())) + .flatten_ok() + .map_ok(|(_, v)| v), + ) +} + +impl Model { + pub fn add_binding( + &mut self, + available_ports: &mut AvailablePorts, + internal_port: u16, + options: BindOptions, + ) -> Result<(), Error> { + self.as_bindings_mut().mutate(|b| { + let info = if let Some(info) = b.remove(&internal_port) { + info.update(available_ports, options)? + } else { + BindInfo::new(available_ports, options)? + }; + b.insert(internal_port, info); + Ok(()) + }) + } +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct RequiresPackageId { + package: PackageId, +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct RequiresHostId { + host: HostId, +} + +pub trait HostApiKind: 'static { + type Params: Send + Sync + 'static; + type InheritedParams: Send + Sync + 'static; + type Inheritance: RefUnwindSafe + OrEmpty + Send + Sync + 'static; + fn inheritance(params: Self::Params, inherited: Self::InheritedParams) -> Self::Inheritance; + fn host_for<'a>( + inheritance: &Self::Inheritance, + db: &'a mut DatabaseModel, + ) -> Result<&'a mut Model, Error>; + fn sync_host( + ctx: &RpcContext, + inheritance: Self::Inheritance, + ) -> impl Future> + Send; +} +pub struct ForPackage; +impl HostApiKind for ForPackage { + type Params = RequiresHostId; + type InheritedParams = PackageId; + type Inheritance = (PackageId, HostId); + fn inheritance( + RequiresHostId { host }: Self::Params, + package: Self::InheritedParams, + ) -> Self::Inheritance { + (package, host) + } + fn host_for<'a>( + (package, host): &Self::Inheritance, + db: &'a mut DatabaseModel, + ) -> Result<&'a mut Model, Error> { + host_for(db, Some(package), host) + } + async fn sync_host(ctx: &RpcContext, (package, host): Self::Inheritance) -> Result<(), Error> { + let service = ctx.services.get(&package).await; + let service_ref = service.as_ref().or_not_found(&package)?; + service_ref.sync_host(host).await?; + Ok(()) + } +} +pub struct ForServer; +impl HostApiKind for ForServer { + type Params = Empty; + type InheritedParams = Empty; + type Inheritance = Empty; + fn inheritance(_: Self::Params, _: Self::InheritedParams) -> Self::Inheritance { + Empty {} + } + fn host_for<'a>( + _: &Self::Inheritance, + db: &'a mut DatabaseModel, + ) -> Result<&'a mut Model, Error> { + host_for(db, None, &HostId::default()) + } + async fn sync_host(ctx: &RpcContext, _: Self::Inheritance) -> Result<(), Error> { + ctx.os_net_service.sync_host(HostId::default()).await + } +} + +pub fn host_api() -> ParentHandler { + ParentHandler::::new() + .subcommand( + "list", + from_fn_async(list_hosts) + .with_inherited(|RequiresPackageId { package }, _| package) + .with_custom_display_fn(|_, ids| { + for id in ids { + println!("{id}") + } + Ok(()) + }) + .with_about("List host IDs available for this service"), + ) + .subcommand( + "address", + address_api::() + .with_inherited(|RequiresPackageId { package }, _| package), + ) + .subcommand( + "binding", + binding::().with_inherited(|RequiresPackageId { package }, _| package), + ) +} + +pub fn server_host_api() -> ParentHandler { + ParentHandler::::new() + .subcommand("address", address_api::()) + .subcommand("binding", binding::()) +} + +pub async fn list_hosts( + ctx: RpcContext, + _: Empty, + package: PackageId, +) -> Result, Error> { + ctx.db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package) + .or_not_found(&package)? + .into_hosts() + .keys() +} diff --git a/core/startos/src/net/interface.rs b/core/startos/src/net/interface.rs deleted file mode 100644 index a055bb277..000000000 --- a/core/startos/src/net/interface.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use indexmap::IndexSet; -pub use models::InterfaceId; -use serde::{Deserialize, Deserializer, Serialize}; -use sqlx::{Executor, Postgres}; -use tracing::instrument; - -use crate::db::model::{InterfaceAddressMap, InterfaceAddresses}; -use crate::net::keys::Key; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::Port; -use crate::{Error, ResultExt}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Interfaces(pub BTreeMap); // TODO -impl Interfaces { - #[instrument(skip_all)] - pub fn validate(&self) -> Result<(), Error> { - for (_, interface) in &self.0 { - interface.validate().with_ctx(|_| { - ( - crate::ErrorKind::ValidateS9pk, - format!("Interface {}", interface.name), - ) - })?; - } - Ok(()) - } - #[instrument(skip_all)] - pub async fn install( - &self, - secrets: &mut Ex, - package_id: &PackageId, - ) -> Result - where - for<'a> &'a mut Ex: Executor<'a, Database = Postgres>, - { - let mut interface_addresses = InterfaceAddressMap(BTreeMap::new()); - for (id, iface) in &self.0 { - let mut addrs = InterfaceAddresses { - tor_address: None, - lan_address: None, - }; - if iface.tor_config.is_some() || iface.lan_config.is_some() { - let key = - Key::for_interface(secrets, Some((package_id.clone(), id.clone()))).await?; - if iface.tor_config.is_some() { - addrs.tor_address = Some(key.tor_address().to_string()); - } - if iface.lan_config.is_some() { - addrs.lan_address = Some(key.local_address()); - } - } - interface_addresses.0.insert(id.clone(), addrs); - } - Ok(interface_addresses) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Interface { - pub name: String, - pub description: String, - pub tor_config: Option, - pub lan_config: Option>, - pub ui: bool, - pub protocols: IndexSet, -} -impl Interface { - #[instrument(skip_all)] - pub fn validate(&self) -> Result<(), color_eyre::eyre::Report> { - if self.tor_config.is_some() && !self.protocols.contains("tcp") { - color_eyre::eyre::bail!("must support tcp to set up a tor hidden service"); - } - if self.lan_config.is_some() && !self.protocols.contains("http") { - color_eyre::eyre::bail!("must support http to set up a lan service"); - } - if self.ui && !(self.protocols.contains("http") || self.protocols.contains("https")) { - color_eyre::eyre::bail!("must support http or https to serve a ui"); - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct TorConfig { - pub port_mapping: BTreeMap, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct LanPortConfig { - pub ssl: bool, - pub internal: u16, -} -impl<'de> Deserialize<'de> for LanPortConfig { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "kebab-case")] - struct PermissiveLanPortConfig { - ssl: bool, - internal: Option, - mapping: Option, - } - - let config = PermissiveLanPortConfig::deserialize(deserializer)?; - Ok(LanPortConfig { - ssl: config.ssl, - internal: config - .internal - .or(config.mapping) - .ok_or_else(|| serde::de::Error::missing_field("internal"))?, - }) - } -} diff --git a/core/startos/src/net/keys.rs b/core/startos/src/net/keys.rs index 504bd276d..2cfcb025d 100644 --- a/core/startos/src/net/keys.rs +++ b/core/startos/src/net/keys.rs @@ -1,385 +1,29 @@ -use std::collections::BTreeMap; +use serde::{Deserialize, Serialize}; -use clap::ArgMatches; -use color_eyre::eyre::eyre; -use models::{Id, InterfaceId, PackageId}; -use openssl::pkey::{PKey, Private}; -use openssl::sha::Sha256; -use openssl::x509::X509; -use p256::elliptic_curve::pkcs8::EncodePrivateKey; -use rpc_toolkit::command; -use sqlx::{Acquire, PgExecutor}; -use ssh_key::private::Ed25519PrivateKey; -use torut::onion::{OnionAddressV3, TorSecretKeyV3}; -use zeroize::Zeroize; - -use crate::config::{configure, ConfigureContext}; -use crate::context::RpcContext; -use crate::control::restart; -use crate::disk::fsck::RequiresReboot; -use crate::net::ssl::CertPair; +use crate::account::AccountInfo; +use crate::net::acme::AcmeCertStore; +use crate::net::ssl::CertStore; +use crate::net::tor::OnionStore; use crate::prelude::*; -use crate::util::crypto::ed25519_expand_key; - -// TODO: delete once we may change tor addresses -async fn compat( - secrets: impl PgExecutor<'_>, - interface: &Option<(PackageId, InterfaceId)>, -) -> Result, Error> { - if let Some((package, interface)) = interface { - if let Some(r) = sqlx::query!( - "SELECT key FROM tor WHERE package = $1 AND interface = $2", - package, - interface - ) - .fetch_optional(secrets) - .await? - { - Ok(Some(<[u8; 64]>::try_from(r.key).map_err(|e| { - Error::new( - eyre!("expected vec of len 64, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?)) - } else { - Ok(None) - } - } else if let Some(key) = sqlx::query!("SELECT tor_key FROM account WHERE id = 0") - .fetch_one(secrets) - .await? - .tor_key - { - Ok(Some(<[u8; 64]>::try_from(key).map_err(|e| { - Error::new( - eyre!("expected vec of len 64, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?)) - } else { - Ok(None) - } -} -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Key { - interface: Option<(PackageId, InterfaceId)>, - base: [u8; 32], - tor_key: [u8; 64], // Does NOT necessarily match base -} -impl Key { - pub fn interface(&self) -> Option<(PackageId, InterfaceId)> { - self.interface.clone() - } - pub fn as_bytes(&self) -> [u8; 32] { - self.base - } - pub fn internal_address(&self) -> String { - self.interface - .as_ref() - .map(|(pkg_id, _)| format!("{}.embassy", pkg_id)) - .unwrap_or_else(|| "embassy".to_owned()) - } - pub fn tor_key(&self) -> TorSecretKeyV3 { - self.tor_key.into() - } - pub fn tor_address(&self) -> OnionAddressV3 { - self.tor_key().public().get_onion_address() - } - pub fn base_address(&self) -> String { - self.tor_key() - .public() - .get_onion_address() - .get_address_without_dot_onion() - } - pub fn local_address(&self) -> String { - self.base_address() + ".local" - } - pub fn openssl_key_ed25519(&self) -> PKey { - PKey::private_key_from_raw_bytes(&self.base, openssl::pkey::Id::ED25519).unwrap() - } - pub fn openssl_key_nistp256(&self) -> PKey { - let mut buf = self.base; - loop { - if let Ok(k) = p256::SecretKey::from_slice(&buf) { - return PKey::private_key_from_pkcs8(&*k.to_pkcs8_der().unwrap().as_bytes()) - .unwrap(); - } - let mut sha = Sha256::new(); - sha.update(&buf); - buf = sha.finish(); - } - } - pub fn ssh_key(&self) -> Ed25519PrivateKey { - Ed25519PrivateKey::from_bytes(&self.base) - } - pub(crate) fn from_pair( - interface: Option<(PackageId, InterfaceId)>, - bytes: [u8; 32], - tor_key: [u8; 64], - ) -> Self { - Self { - interface, - tor_key, - base: bytes, - } - } - pub fn from_bytes(interface: Option<(PackageId, InterfaceId)>, bytes: [u8; 32]) -> Self { - Self::from_pair(interface, bytes, ed25519_expand_key(&bytes)) - } - pub fn new(interface: Option<(PackageId, InterfaceId)>) -> Self { - Self::from_bytes(interface, rand::random()) - } - pub(super) fn with_certs(self, certs: CertPair, int: X509, root: X509) -> KeyInfo { - KeyInfo { - key: self, - certs, - int, - root, - } - } - pub async fn for_package( - secrets: impl PgExecutor<'_>, - package: &PackageId, - ) -> Result, Error> { - sqlx::query!( - r#" - SELECT - network_keys.package, - network_keys.interface, - network_keys.key, - tor.key AS "tor_key?" - FROM - network_keys - LEFT JOIN - tor - ON - network_keys.package = tor.package - AND - network_keys.interface = tor.interface - WHERE - network_keys.package = $1 - "#, - package - ) - .fetch_all(secrets) - .await? - .into_iter() - .map(|row| { - let interface = Some(( - package.clone(), - InterfaceId::from(Id::try_from(row.interface)?), - )); - let bytes = row.key.try_into().map_err(|e: Vec| { - Error::new( - eyre!("Invalid length for network key {} expected 32", e.len()), - crate::ErrorKind::Database, - ) - })?; - Ok(match row.tor_key { - Some(tor_key) => Key::from_pair( - interface, - bytes, - tor_key.try_into().map_err(|e: Vec| { - Error::new( - eyre!("Invalid length for tor key {} expected 64", e.len()), - crate::ErrorKind::Database, - ) - })?, - ), - None => Key::from_bytes(interface, bytes), - }) - }) - .collect() - } - pub async fn for_interface( - secrets: &mut Ex, - interface: Option<(PackageId, InterfaceId)>, - ) -> Result - where - for<'a> &'a mut Ex: PgExecutor<'a>, - { - let tentative = rand::random::<[u8; 32]>(); - let actual = if let Some((pkg, iface)) = &interface { - let k = tentative.as_slice(); - let actual = sqlx::query!( - "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO UPDATE SET package = EXCLUDED.package RETURNING key", - pkg, - iface, - k, - ) - .fetch_one(&mut *secrets) - .await?.key; - let mut bytes = tentative; - bytes.clone_from_slice(actual.get(0..32).ok_or_else(|| { - Error::new( - eyre!("Invalid key size returned from DB"), - crate::ErrorKind::Database, - ) - })?); - bytes - } else { - let actual = sqlx::query!("SELECT network_key FROM account WHERE id = 0") - .fetch_one(&mut *secrets) - .await? - .network_key; - let mut bytes = tentative; - bytes.clone_from_slice(actual.get(0..32).ok_or_else(|| { - Error::new( - eyre!("Invalid key size returned from DB"), - crate::ErrorKind::Database, - ) - })?); - bytes +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct KeyStore { + pub onion: OnionStore, + pub local_certs: CertStore, + #[serde(default)] + pub acme: AcmeCertStore, +} +impl KeyStore { + pub fn new(account: &AccountInfo) -> Result { + let mut res = Self { + onion: OnionStore::new(), + local_certs: CertStore::new(account)?, + acme: AcmeCertStore::new(), }; - let mut res = Self::from_bytes(interface, actual); - if let Some(tor_key) = compat(secrets, &res.interface).await? { - res.tor_key = tor_key; + for tor_key in account.tor_keys.iter().cloned() { + res.onion.insert(tor_key); } Ok(res) } } -impl Drop for Key { - fn drop(&mut self) { - self.base.zeroize(); - self.tor_key.zeroize(); - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct KeyInfo { - key: Key, - certs: CertPair, - int: X509, - root: X509, -} -impl KeyInfo { - pub fn key(&self) -> &Key { - &self.key - } - pub fn certs(&self) -> &CertPair { - &self.certs - } - pub fn int_ca(&self) -> &X509 { - &self.int - } - pub fn root_ca(&self) -> &X509 { - &self.root - } - pub fn fullchain_ed25519(&self) -> Vec<&X509> { - vec![&self.certs.ed25519, &self.int, &self.root] - } - pub fn fullchain_nistp256(&self) -> Vec<&X509> { - vec![&self.certs.nistp256, &self.int, &self.root] - } -} - -#[test] -pub fn test_keygen() { - let key = Key::new(None); - key.tor_key(); - key.openssl_key_nistp256(); -} - -fn display_requires_reboot(arg: RequiresReboot, _matches: &ArgMatches) { - if arg.0 { - println!("Server must be restarted for changes to take effect"); - } -} - -#[command(rename = "rotate-key", display(display_requires_reboot))] -pub async fn rotate_key( - #[context] ctx: RpcContext, - #[arg] package: Option, - #[arg] interface: Option, -) -> Result { - let mut pgcon = ctx.secret_store.acquire().await?; - let mut tx = pgcon.begin().await?; - if let Some(package) = package { - let Some(interface) = interface else { - return Err(Error::new( - eyre!("Must specify interface"), - ErrorKind::InvalidRequest, - )); - }; - sqlx::query!( - "DELETE FROM tor WHERE package = $1 AND interface = $2", - &package, - &interface, - ) - .execute(&mut *tx) - .await?; - sqlx::query!( - "DELETE FROM network_keys WHERE package = $1 AND interface = $2", - &package, - &interface, - ) - .execute(&mut *tx) - .await?; - let new_key = - Key::for_interface(&mut *tx, Some((package.clone(), interface.clone()))).await?; - let needs_config = ctx - .db - .mutate(|v| { - let installed = v - .as_package_data_mut() - .as_idx_mut(&package) - .or_not_found(&package)? - .as_installed_mut() - .or_not_found("installed")?; - let addrs = installed - .as_interface_addresses_mut() - .as_idx_mut(&interface) - .or_not_found(&interface)?; - if let Some(lan) = addrs.as_lan_address_mut().transpose_mut() { - lan.ser(&new_key.local_address())?; - } - if let Some(lan) = addrs.as_tor_address_mut().transpose_mut() { - lan.ser(&new_key.tor_address().to_string())?; - } - - if installed - .as_manifest() - .as_config() - .transpose_ref() - .is_some() - { - installed - .as_status_mut() - .as_configured_mut() - .replace(&false) - } else { - Ok(false) - } - }) - .await?; - tx.commit().await?; - if needs_config { - configure( - &ctx, - &package, - ConfigureContext { - breakages: BTreeMap::new(), - timeout: None, - config: None, - overrides: BTreeMap::new(), - dry_run: false, - }, - ) - .await?; - } else { - restart(ctx, package).await?; - } - Ok(RequiresReboot(false)) - } else { - sqlx::query!("UPDATE account SET tor_key = NULL, network_key = gen_random_bytes(32)") - .execute(&mut *tx) - .await?; - let new_key = Key::for_interface(&mut *tx, None).await?; - let url = format!("https://{}", new_key.tor_address()).parse()?; - ctx.db - .mutate(|v| v.as_server_info_mut().as_tor_address_mut().ser(&url)) - .await?; - tx.commit().await?; - Ok(RequiresReboot(true)) - } -} diff --git a/core/startos/src/net/mdns.rs b/core/startos/src/net/mdns.rs index 21054241d..af5d128a8 100644 --- a/core/startos/src/net/mdns.rs +++ b/core/startos/src/net/mdns.rs @@ -1,14 +1,10 @@ -use std::collections::BTreeMap; use std::net::Ipv4Addr; -use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; -use tokio::process::{Child, Command}; -use tokio::sync::Mutex; -use tracing::instrument; +use tokio::process::Command; +use crate::prelude::*; use crate::util::Invoke; -use crate::{Error, ResultExt}; pub async fn resolve_mdns(hostname: &str) -> Result { Ok(String::from_utf8( @@ -30,71 +26,3 @@ pub async fn resolve_mdns(hostname: &str) -> Result { .trim() .parse()?) } - -pub struct MdnsController(Mutex); -impl MdnsController { - pub async fn init() -> Result { - Ok(MdnsController(Mutex::new( - MdnsControllerInner::init().await?, - ))) - } - pub async fn add(&self, alias: String) -> Result, Error> { - self.0.lock().await.add(alias).await - } - pub async fn gc(&self, alias: String) -> Result<(), Error> { - self.0.lock().await.gc(alias).await - } -} - -pub struct MdnsControllerInner { - alias_cmd: Option, - services: BTreeMap>, -} - -impl MdnsControllerInner { - #[instrument(skip_all)] - async fn init() -> Result { - let mut res = MdnsControllerInner { - alias_cmd: None, - services: BTreeMap::new(), - }; - res.sync().await?; - Ok(res) - } - #[instrument(skip_all)] - async fn sync(&mut self) -> Result<(), Error> { - if let Some(mut cmd) = self.alias_cmd.take() { - cmd.kill().await.with_kind(crate::ErrorKind::Network)?; - } - self.alias_cmd = Some( - Command::new("avahi-alias") - .kill_on_drop(true) - .args( - self.services - .iter() - .filter(|(_, rc)| rc.strong_count() > 0) - .map(|(s, _)| s), - ) - .spawn()?, - ); - Ok(()) - } - async fn add(&mut self, alias: String) -> Result, Error> { - let rc = if let Some(rc) = Weak::upgrade(&self.services.remove(&alias).unwrap_or_default()) - { - rc - } else { - Arc::new(()) - }; - self.services.insert(alias, Arc::downgrade(&rc)); - self.sync().await?; - Ok(rc) - } - async fn gc(&mut self, alias: String) -> Result<(), Error> { - if let Some(rc) = Weak::upgrade(&self.services.remove(&alias).unwrap_or_default()) { - self.services.insert(alias, Arc::downgrade(&rc)); - } - self.sync().await?; - Ok(()) - } -} diff --git a/core/startos/src/net/mod.rs b/core/startos/src/net/mod.rs index 50935fb18..49d3560ef 100644 --- a/core/startos/src/net/mod.rs +++ b/core/startos/src/net/mod.rs @@ -1,17 +1,14 @@ -use std::sync::Arc; +use rpc_toolkit::{Context, HandlerExt, ParentHandler}; -use futures::future::BoxFuture; -use hyper::{Body, Error as HyperError, Request, Response}; -use rpc_toolkit::command; - -use crate::Error; - -pub mod dhcp; +pub mod acme; pub mod dns; -pub mod interface; +pub mod forward; +pub mod host; pub mod keys; pub mod mdns; pub mod net_controller; +pub mod network_interface; +pub mod service_interface; pub mod ssl; pub mod static_server; pub mod tor; @@ -20,13 +17,23 @@ pub mod vhost; pub mod web_server; pub mod wifi; -pub const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl"; - -#[command(subcommands(tor::tor, dhcp::dhcp, ssl::ssl, keys::rotate_key))] -pub fn net() -> Result<(), Error> { - Ok(()) +pub fn net() -> ParentHandler { + ParentHandler::new() + .subcommand( + "tor", + tor::tor::().with_about("Tor commands such as list-services, logs, and reset"), + ) + .subcommand( + "acme", + acme::acme::().with_about("Setup automatic clearnet certificate acquisition"), + ) + .subcommand( + "network-interface", + network_interface::network_interface_api::() + .with_about("View and edit network interface configurations"), + ) + .subcommand( + "vhost", + vhost::vhost_api::().with_about("Manage ssl virtual host proxy"), + ) } - -pub type HttpHandler = Arc< - dyn Fn(Request) -> BoxFuture<'static, Result, HyperError>> + Send + Sync, ->; diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index e2e77ed68..c3d0e8676 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -1,148 +1,72 @@ -use std::collections::BTreeMap; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{Ipv4Addr, SocketAddr}; use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; -use models::InterfaceId; -use sqlx::PgExecutor; +use imbl::OrdMap; +use imbl_value::InternedString; +use ipnet::IpNet; +use models::{HostId, OptionExt, PackageId}; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; +use crate::db::model::Database; use crate::error::ErrorCollection; use crate::hostname::Hostname; use crate::net::dns::DnsController; -use crate::net::keys::Key; -use crate::net::mdns::MdnsController; -use crate::net::ssl::{export_cert, export_key, SslManager}; +use crate::net::forward::LanPortForwardController; +use crate::net::host::address::HostAddress; +use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; +use crate::net::host::{host_for, Host, Hosts}; +use crate::net::network_interface::NetworkInterfaceController; +use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname}; use crate::net::tor::TorController; -use crate::net::vhost::{AlpnInfo, VHostController}; -use crate::s9pk::manifest::PackageId; -use crate::volume::cert_dir; -use crate::{Error, HOST_IP}; +use crate::net::utils::ipv6_is_local; +use crate::net::vhost::{AlpnInfo, TargetInfo, VHostController}; +use crate::prelude::*; +use crate::util::serde::MaybeUtf8String; +use crate::HOST_IP; pub struct NetController { + pub(crate) db: TypedPatchDb, pub(super) tor: TorController, - pub(super) mdns: MdnsController, pub(super) vhost: VHostController, + pub(crate) net_iface: Arc, pub(super) dns: DnsController, - pub(super) ssl: Arc, - pub(super) os_bindings: Vec>, + pub(super) forward: LanPortForwardController, + pub(super) server_hostnames: Vec>, } impl NetController { - #[instrument(skip_all)] pub async fn init( + db: TypedPatchDb, tor_control: SocketAddr, tor_socks: SocketAddr, - dns_bind: &[SocketAddr], - ssl: SslManager, hostname: &Hostname, - os_key: &Key, ) -> Result { - let ssl = Arc::new(ssl); - let mut res = Self { + let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); + Ok(Self { + db: db.clone(), tor: TorController::new(tor_control, tor_socks), - mdns: MdnsController::init().await?, - vhost: VHostController::new(ssl.clone()), - dns: DnsController::init(dns_bind).await?, - ssl, - os_bindings: Vec::new(), - }; - res.add_os_bindings(hostname, os_key).await?; - Ok(res) - } - - async fn add_os_bindings(&mut self, hostname: &Hostname, key: &Key) -> Result<(), Error> { - let alpn = Err(AlpnInfo::Specified(vec!["http/1.1".into(), "h2".into()])); - - // Internal DNS - self.vhost - .add( - key.clone(), + vhost: VHostController::new(db, net_iface.clone()), + dns: DnsController::init(net_iface.lxcbr_status()).await?, + forward: LanPortForwardController::new(net_iface.subscribe()), + net_iface, + server_hostnames: vec![ + // LAN IP + None, + // Internal DNS Some("embassy".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?; - self.os_bindings - .push(self.dns.add(None, HOST_IP.into()).await?); - - // LAN IP - self.os_bindings.push( - self.vhost - .add( - key.clone(), - None, - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - - // localhost - self.os_bindings.push( - self.vhost - .add( - key.clone(), - Some("localhost".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - self.os_bindings.push( - self.vhost - .add( - key.clone(), - Some(hostname.no_dot_host_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - - // LAN mDNS - self.os_bindings.push( - self.vhost - .add( - key.clone(), - Some(hostname.local_domain_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - - // Tor (http) - self.os_bindings.push( - self.tor - .add(key.tor_key(), 80, ([127, 0, 0, 1], 80).into()) - .await?, - ); - - // Tor (https) - self.os_bindings.push( - self.vhost - .add( - key.clone(), - Some(key.tor_address().to_string()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - self.os_bindings.push( - self.tor - .add(key.tor_key(), 443, ([127, 0, 0, 1], 443).into()) - .await?, - ); - - Ok(()) + Some("startos".into()), + // localhost + Some("localhost".into()), + Some(hostname.no_dot_host_name()), + // LAN mDNS + Some(hostname.local_domain_name()), + ], + }) } #[instrument(skip_all)] @@ -153,73 +77,65 @@ impl NetController { ) -> Result { let dns = self.dns.add(Some(package.clone()), ip).await?; - Ok(NetService { - shutdown: false, - id: package, + let res = NetService::new(NetServiceData { + id: Some(package), ip, dns, controller: Arc::downgrade(self), - tor: BTreeMap::new(), - lan: BTreeMap::new(), - }) + binds: BTreeMap::new(), + })?; + res.clear_bindings(Default::default()).await?; + Ok(res) } - async fn add_tor( - &self, - key: &Key, - external: u16, - target: SocketAddr, - ) -> Result>, Error> { - let mut rcs = Vec::with_capacity(1); - rcs.push(self.tor.add(key.tor_key(), external, target).await?); - Ok(rcs) - } + pub async fn os_bindings(self: &Arc) -> Result { + let dns = self.dns.add(None, HOST_IP.into()).await?; - async fn remove_tor(&self, key: &Key, external: u16, rcs: Vec>) -> Result<(), Error> { - drop(rcs); - self.tor.gc(Some(key.tor_key()), Some(external)).await - } + let service = NetService::new(NetServiceData { + id: None, + ip: [127, 0, 0, 1].into(), + dns, + controller: Arc::downgrade(self), + binds: BTreeMap::new(), + })?; + service.clear_bindings(Default::default()).await?; + service + .bind( + HostId::default(), + 80, + BindOptions { + preferred_external_port: 80, + add_ssl: Some(AddSslOptions { + preferred_external_port: 443, + alpn: Some(AlpnInfo::Specified(vec![ + MaybeUtf8String("http/1.1".into()), + MaybeUtf8String("h2".into()), + ])), + }), + secure: None, + }, + ) + .await?; - async fn add_lan( - &self, - key: Key, - external: u16, - target: SocketAddr, - connect_ssl: Result<(), AlpnInfo>, - ) -> Result>, Error> { - let mut rcs = Vec::with_capacity(2); - rcs.push( - self.vhost - .add( - key.clone(), - Some(key.local_address()), - external, - target.into(), - connect_ssl, - ) - .await?, - ); - rcs.push(self.mdns.add(key.base_address()).await?); - Ok(rcs) + Ok(service) } +} - async fn remove_lan(&self, key: &Key, external: u16, rcs: Vec>) -> Result<(), Error> { - drop(rcs); - self.mdns.gc(key.base_address()).await?; - self.vhost.gc(Some(key.local_address()), external).await - } +#[derive(Default, Debug)] +struct HostBinds { + forwards: BTreeMap)>, + vhosts: BTreeMap<(Option, u16), (TargetInfo, Arc<()>)>, + tor: BTreeMap, Vec>)>, } -pub struct NetService { - shutdown: bool, - id: PackageId, +pub struct NetServiceData { + id: Option, ip: Ipv4Addr, dns: Arc<()>, controller: Weak, - tor: BTreeMap<(InterfaceId, u16), (Key, Vec>)>, - lan: BTreeMap<(InterfaceId, u16), (Key, Vec>)>, + binds: BTreeMap, } -impl NetService { +impl NetServiceData { fn net_controller(&self) -> Result, Error> { Weak::upgrade(&self.controller).ok_or_else(|| { Error::new( @@ -228,116 +144,577 @@ impl NetService { ) }) } - pub async fn add_tor( + + async fn clear_bindings( &mut self, - secrets: &mut Ex, - id: InterfaceId, - external: u16, - internal: u16, - ) -> Result<(), Error> - where - for<'a> &'a mut Ex: PgExecutor<'a>, - { - let key = Key::for_interface(secrets, Some((self.id.clone(), id.clone()))).await?; - let ctrl = self.net_controller()?; - let tor_idx = (id, external); - let mut tor = self - .tor - .remove(&tor_idx) - .unwrap_or_else(|| (key.clone(), Vec::new())); - tor.1.append( - &mut ctrl - .add_tor(&key, external, SocketAddr::new(self.ip.into(), internal)) - .await?, - ); - self.tor.insert(tor_idx, tor); - Ok(()) - } - pub async fn remove_tor(&mut self, id: InterfaceId, external: u16) -> Result<(), Error> { - let ctrl = self.net_controller()?; - if let Some((key, rcs)) = self.tor.remove(&(id, external)) { - ctrl.remove_tor(&key, external, rcs).await?; + ctrl: &NetController, + except: BTreeSet, + ) -> Result<(), Error> { + if let Some(pkg_id) = &self.id { + let hosts = ctrl + .db + .mutate(|db| { + let mut res = Hosts::default(); + for (host_id, host) in db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(pkg_id) + .or_not_found(pkg_id)? + .as_hosts_mut() + .as_entries_mut()? + { + host.as_bindings_mut().mutate(|b| { + for (internal_port, info) in b { + if !except.contains(&BindId { + id: host_id.clone(), + internal_port: *internal_port, + }) { + info.disable(); + } + } + Ok(()) + })?; + res.0.insert(host_id, host.de()?); + } + Ok(res) + }) + .await?; + let mut errors = ErrorCollection::new(); + for (id, host) in hosts.0 { + errors.handle(self.update(ctrl, id, host).await); + } + errors.into_result() + } else { + let host = ctrl + .db + .mutate(|db| { + let host = db.as_public_mut().as_server_info_mut().as_host_mut(); + host.as_bindings_mut().mutate(|b| { + for (internal_port, info) in b { + if !except.contains(&BindId { + id: HostId::default(), + internal_port: *internal_port, + }) { + info.disable(); + } + } + Ok(()) + })?; + host.de() + }) + .await?; + self.update(ctrl, HostId::default(), host).await } - Ok(()) } - pub async fn add_lan( - &mut self, - secrets: &mut Ex, - id: InterfaceId, - external: u16, - internal: u16, - connect_ssl: Result<(), AlpnInfo>, - ) -> Result<(), Error> - where - for<'a> &'a mut Ex: PgExecutor<'a>, - { - let key = Key::for_interface(secrets, Some((self.id.clone(), id.clone()))).await?; - let ctrl = self.net_controller()?; - let lan_idx = (id, external); - let mut lan = self - .lan - .remove(&lan_idx) - .unwrap_or_else(|| (key.clone(), Vec::new())); - lan.1.append( - &mut ctrl - .add_lan( - key, + + async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> { + let mut forwards: BTreeMap = BTreeMap::new(); + let mut vhosts: BTreeMap<(Option, u16), TargetInfo> = BTreeMap::new(); + let mut tor: BTreeMap)> = + BTreeMap::new(); + let mut hostname_info: BTreeMap> = BTreeMap::new(); + let binds = self.binds.entry(id.clone()).or_default(); + + let peek = ctrl.db.peek().await; + + // LAN + let server_info = peek.as_public().as_server_info(); + let net_ifaces = ctrl.net_iface.ip_info(); + let hostname = server_info.as_hostname().de()?; + for (port, bind) in &host.bindings { + if !bind.enabled { + continue; + } + if bind.net.assigned_port.is_some() || bind.net.assigned_ssl_port.is_some() { + let mut hostnames = BTreeSet::new(); + if let Some(ssl) = &bind.options.add_ssl { + let external = bind + .net + .assigned_ssl_port + .or_not_found("assigned ssl port")?; + let addr = (self.ip, *port).into(); + let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { + Err(alpn) + } else { + if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { + Ok(()) + } else { + Err(AlpnInfo::Reflect) + } + }; + for hostname in ctrl.server_hostnames.iter().cloned() { + vhosts.insert( + (hostname, external), + TargetInfo { + public: bind.net.public, + acme: None, + addr, + connect_ssl: connect_ssl.clone(), + }, + ); + } + for address in host.addresses() { + match address { + HostAddress::Onion { address } => { + let hostname = InternedString::from_display(&address); + if hostnames.insert(hostname.clone()) { + vhosts.insert( + (Some(hostname), external), + TargetInfo { + public: false, + acme: None, + addr, + connect_ssl: connect_ssl.clone(), + }, + ); + } + } + HostAddress::Domain { + address, + public, + acme, + } => { + if hostnames.insert(address.clone()) { + let address = Some(address.clone()); + if ssl.preferred_external_port == 443 { + if public && bind.net.public { + vhosts.insert( + (address.clone(), 5443), + TargetInfo { + public: false, + acme: acme.clone(), + addr, + connect_ssl: connect_ssl.clone(), + }, + ); + } + vhosts.insert( + (address.clone(), 443), + TargetInfo { + public: public && bind.net.public, + acme, + addr, + connect_ssl: connect_ssl.clone(), + }, + ); + } else { + vhosts.insert( + (address.clone(), external), + TargetInfo { + public: public && bind.net.public, + acme, + addr, + connect_ssl: connect_ssl.clone(), + }, + ); + } + } + } + } + } + } + if let Some(security) = bind.options.secure { + if bind.options.add_ssl.is_some() && security.ssl { + // doesn't make sense to have 2 listening ports, both with ssl + } else { + let external = bind.net.assigned_port.or_not_found("assigned lan port")?; + forwards.insert(external, ((self.ip, *port).into(), bind.net.public)); + } + } + let mut bind_hostname_info: Vec = + hostname_info.remove(port).unwrap_or_default(); + for (interface, public, ip_info) in + net_ifaces.iter().filter_map(|(interface, info)| { + if let Some(ip_info) = &info.ip_info { + Some((interface, info.public(), ip_info)) + } else { + None + } + }) + { + if !public { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Local { + value: InternedString::from_display(&{ + let hostname = &hostname; + lazy_format!("{hostname}.local") + }), + port: bind.net.assigned_port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + for address in host.addresses() { + if let HostAddress::Domain { + address, + public: domain_public, + .. + } = address + { + if !public || (domain_public && bind.net.public) { + if bind + .options + .add_ssl + .as_ref() + .map_or(false, |ssl| ssl.preferred_external_port == 443) + { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: public && domain_public && bind.net.public, // TODO: check if port forward is active + hostname: IpHostname::Domain { + domain: address.clone(), + subdomain: None, + port: None, + ssl_port: Some(443), + }, + }); + } else { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public, + hostname: IpHostname::Domain { + domain: address.clone(), + subdomain: None, + port: bind.net.assigned_port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + } + } + } + if !public || bind.net.public { + if let Some(wan_ip) = ip_info.wan_ip.filter(|_| public) { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public, + hostname: IpHostname::Ipv4 { + value: wan_ip, + port: bind.net.assigned_port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + for ipnet in &ip_info.subnets { + match ipnet { + IpNet::V4(net) => { + if !public { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public, + hostname: IpHostname::Ipv4 { + value: net.addr(), + port: bind.net.assigned_port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + } + IpNet::V6(net) => { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: public && !ipv6_is_local(net.addr()), + hostname: IpHostname::Ipv6 { + value: net.addr(), + scope_id: ip_info.scope_id, + port: bind.net.assigned_port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + } + } + } + } + hostname_info.insert(*port, bind_hostname_info); + } + } + + struct TorHostnamePorts { + non_ssl: Option, + ssl: Option, + } + let mut tor_hostname_ports = BTreeMap::::new(); + let mut tor_binds = OrdMap::::new(); + for (internal, info) in &host.bindings { + if !info.enabled { + continue; + } + tor_binds.insert( + info.options.preferred_external_port, + SocketAddr::from((self.ip, *internal)), + ); + if let (Some(ssl), Some(ssl_internal)) = + (&info.options.add_ssl, info.net.assigned_ssl_port) + { + tor_binds.insert( + ssl.preferred_external_port, + SocketAddr::from(([127, 0, 0, 1], ssl_internal)), + ); + tor_hostname_ports.insert( + *internal, + TorHostnamePorts { + non_ssl: Some(info.options.preferred_external_port) + .filter(|p| *p != ssl.preferred_external_port), + ssl: Some(ssl.preferred_external_port), + }, + ); + } else { + tor_hostname_ports.insert( + *internal, + TorHostnamePorts { + non_ssl: Some(info.options.preferred_external_port), + ssl: None, + }, + ); + } + } + + for tor_addr in host.onions.iter() { + let key = peek + .as_private() + .as_key_store() + .as_onion() + .get_key(tor_addr)?; + tor.insert(key.public().get_onion_address(), (key, tor_binds.clone())); + for (internal, ports) in &tor_hostname_ports { + let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default(); + bind_hostname_info.push(HostnameInfo::Onion { + hostname: OnionHostname { + value: tor_addr.to_string(), + port: ports.non_ssl, + ssl_port: ports.ssl, + }, + }); + hostname_info.insert(*internal, bind_hostname_info); + } + } + + let all = binds + .forwards + .keys() + .chain(forwards.keys()) + .copied() + .collect::>(); + for external in all { + let mut prev = binds.forwards.remove(&external); + if let Some((internal, public)) = forwards.remove(&external) { + prev = prev.filter(|(i, p, _)| i == &internal && *p == public); + binds.forwards.insert( external, - SocketAddr::new(self.ip.into(), internal), - connect_ssl, - ) - .await?, - ); - self.lan.insert(lan_idx, lan); + if let Some(prev) = prev { + prev + } else { + ( + internal, + public, + ctrl.forward.add(external, public, internal).await?, + ) + }, + ); + } + } + ctrl.forward.gc().await?; + + let all = binds + .vhosts + .keys() + .chain(vhosts.keys()) + .cloned() + .collect::>(); + for key in all { + let mut prev = binds.vhosts.remove(&key); + if let Some(target) = vhosts.remove(&key) { + prev = prev.filter(|(t, _)| t == &target); + binds.vhosts.insert( + key.clone(), + if let Some(prev) = prev { + prev + } else { + (target.clone(), ctrl.vhost.add(key.0, key.1, target)?) + }, + ); + } else { + if let Some((_, rc)) = prev { + drop(rc); + ctrl.vhost.gc(key.0, key.1); + } + } + } + + let all = binds + .tor + .keys() + .chain(tor.keys()) + .cloned() + .collect::>(); + for onion in all { + let mut prev = binds.tor.remove(&onion); + if let Some((key, tor_binds)) = tor.remove(&onion) { + prev = prev.filter(|(b, _)| b == &tor_binds); + binds.tor.insert( + onion, + if let Some(prev) = prev { + prev + } else { + let rcs = ctrl + .tor + .add(key, tor_binds.iter().map(|(k, v)| (*k, *v)).collect()) + .await?; + (tor_binds, rcs) + }, + ); + } else { + if let Some((_, rc)) = prev { + drop(rc); + ctrl.tor.gc(Some(onion), None).await?; + } + } + } + + ctrl.db + .mutate(|db| { + host_for(db, self.id.as_ref(), &id)? + .as_hostname_info_mut() + .ser(&hostname_info) + }) + .await?; Ok(()) } - pub async fn remove_lan(&mut self, id: InterfaceId, external: u16) -> Result<(), Error> { + + async fn update_all(&mut self) -> Result<(), Error> { let ctrl = self.net_controller()?; - if let Some((key, rcs)) = self.lan.remove(&(id, external)) { - ctrl.remove_lan(&key, external, rcs).await?; + if let Some(id) = self.id.clone() { + for (host_id, host) in ctrl + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(&id) + .or_not_found(&id)? + .as_hosts() + .as_entries()? + { + tracing::info!("Updating host {host_id} for {id}"); + self.update(&*ctrl, host_id.clone(), host.de()?).await?; + tracing::info!("Updated host {host_id} for {id}"); + } + } else { + tracing::info!("Updating host for Main UI"); + self.update( + &*ctrl, + HostId::default(), + ctrl.db + .peek() + .await + .as_public() + .as_server_info() + .as_host() + .de()?, + ) + .await?; + tracing::info!("Updated host for Main UI"); } Ok(()) } - pub async fn export_cert( +} + +pub struct NetService { + shutdown: bool, + data: Arc>, + sync_task: JoinHandle<()>, +} +impl NetService { + fn dummy() -> Self { + Self { + shutdown: true, + data: Arc::new(Mutex::new(NetServiceData { + id: None, + ip: Ipv4Addr::new(0, 0, 0, 0), + dns: Default::default(), + controller: Default::default(), + binds: BTreeMap::new(), + })), + sync_task: tokio::spawn(futures::future::ready(())), + } + } + + fn new(data: NetServiceData) -> Result { + let mut ip_info = data.net_controller()?.net_iface.subscribe(); + let data = Arc::new(Mutex::new(data)); + let thread_data = data.clone(); + let sync_task = tokio::spawn(async move { + loop { + if let Err(e) = thread_data.lock().await.update_all().await { + tracing::error!("Failed to update network info: {e}"); + tracing::debug!("{e:?}"); + } + ip_info.changed().await; + } + }); + Ok(Self { + shutdown: false, + data, + sync_task, + }) + } + + pub async fn bind( &self, - secrets: &mut Ex, - id: &InterfaceId, - ip: IpAddr, - ) -> Result<(), Error> - where - for<'a> &'a mut Ex: PgExecutor<'a>, - { - let key = Key::for_interface(secrets, Some((self.id.clone(), id.clone()))).await?; - let ctrl = self.net_controller()?; - let cert = ctrl.ssl.with_certs(key, ip).await?; - let cert_dir = cert_dir(&self.id, id); - tokio::fs::create_dir_all(&cert_dir).await?; - export_key( - &cert.key().openssl_key_nistp256(), - &cert_dir.join(format!("{id}.key.pem")), - ) - .await?; - export_cert( - &cert.fullchain_nistp256(), - &cert_dir.join(format!("{id}.cert.pem")), - ) - .await?; // TODO: can upgrade to ed25519? - Ok(()) + id: HostId, + internal_port: u16, + options: BindOptions, + ) -> Result<(), Error> { + let mut data = self.data.lock().await; + let pkg_id = &data.id; + let ctrl = data.net_controller()?; + let host = ctrl + .db + .mutate(|db| { + let mut ports = db.as_private().as_available_ports().de()?; + let host = host_for(db, pkg_id.as_ref(), &id)?; + host.add_binding(&mut ports, internal_port, options)?; + let host = host.de()?; + db.as_private_mut().as_available_ports_mut().ser(&ports)?; + Ok(host) + }) + .await?; + data.update(&*ctrl, id, host).await + } + + pub async fn clear_bindings(&self, except: BTreeSet) -> Result<(), Error> { + let mut data = self.data.lock().await; + let ctrl = data.net_controller()?; + data.clear_bindings(&*ctrl, except).await + } + + pub async fn update(&self, id: HostId, host: Host) -> Result<(), Error> { + let mut data = self.data.lock().await; + let ctrl = data.net_controller()?; + data.update(&*ctrl, id, host).await } + + pub async fn sync_host(&self, id: HostId) -> Result<(), Error> { + let mut data = self.data.lock().await; + let ctrl = data.net_controller()?; + let host = host_for(&mut ctrl.db.peek().await, data.id.as_ref(), &id)?.de()?; + data.update(&*ctrl, id, host).await + } + pub async fn remove_all(mut self) -> Result<(), Error> { - self.shutdown = true; - let mut errors = ErrorCollection::new(); - if let Some(ctrl) = Weak::upgrade(&self.controller) { - for ((_, external), (key, rcs)) in std::mem::take(&mut self.lan) { - errors.handle(ctrl.remove_lan(&key, external, rcs).await); - } - for ((_, external), (key, rcs)) in std::mem::take(&mut self.tor) { - errors.handle(ctrl.remove_tor(&key, external, rcs).await); - } - std::mem::take(&mut self.dns); - errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); - errors.into_result() + self.sync_task.abort(); + let mut data = self.data.lock().await; + if let Some(ctrl) = Weak::upgrade(&data.controller) { + self.shutdown = true; + data.clear_bindings(&*ctrl, Default::default()).await?; + + drop(ctrl); + Ok(()) } else { + self.shutdown = true; tracing::warn!("NetService dropped after NetController is shutdown"); Err(Error::new( eyre!("NetController is shutdown"), @@ -345,25 +722,17 @@ impl NetService { )) } } + + pub async fn get_ip(&self) -> Ipv4Addr { + self.data.lock().await.ip + } } impl Drop for NetService { fn drop(&mut self) { if !self.shutdown { - tracing::debug!("Dropping NetService for {}", self.id); - let svc = std::mem::replace( - self, - NetService { - shutdown: true, - id: Default::default(), - ip: Ipv4Addr::new(0, 0, 0, 0), - dns: Default::default(), - controller: Default::default(), - tor: Default::default(), - lan: Default::default(), - }, - ); - tokio::spawn(async move { svc.remove_all().await.unwrap() }); + let svc = std::mem::replace(self, Self::dummy()); + tokio::spawn(async move { svc.remove_all().await.log_err() }); } } } diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/network_interface.rs new file mode 100644 index 000000000..8c99f0de9 --- /dev/null +++ b/core/startos/src/net/network_interface.rs @@ -0,0 +1,1113 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6}; +use std::sync::{Arc, Weak}; +use std::task::Poll; +use std::time::Duration; + +use clap::Parser; +use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use ipnet::IpNet; +use itertools::Itertools; +use nix::net::if_::if_nametoindex; +use patch_db::json_ptr::JsonPointer; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::process::Command; +use ts_rs::TS; +use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream}; +use zbus::zvariant::{ + DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType, Value as ZValue, +}; +use zbus::{proxy, Connection}; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType}; +use crate::db::model::Database; +use crate::net::forward::START9_BRIDGE_IFACE; +use crate::net::utils::{ipv6_is_link_local, ipv6_is_local}; +use crate::net::web_server::Accept; +use crate::prelude::*; +use crate::util::future::Until; +use crate::util::io::open_file; +use crate::util::serde::{display_serializable, HandlerExtSerde}; +use crate::util::sync::{SyncMutex, Watch}; +use crate::util::Invoke; + +pub fn network_interface_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list_interfaces) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + return Ok(display_serializable(format, res)); + } + + let mut table = Table::new(); + table.add_row(row![bc => "INTERFACE", "TYPE", "PUBLIC", "ADDRESSES", "WAN IP"]); + for (iface, info) in res { + table.add_row(row![ + iface, + info.ip_info.as_ref() + .and_then(|ip_info| ip_info.device_type) + .map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")), + info.public(), + info.ip_info.as_ref().map_or_else( + || "".to_owned(), + |ip_info| ip_info.subnets + .iter() + .map(|ipnet| match ipnet.addr() { + IpAddr::V4(ip) => format!("{ip}/{}", ipnet.prefix_len()), + IpAddr::V6(ip) => format!( + "[{ip}%{}]/{}", + ip_info.scope_id, + ipnet.prefix_len() + ), + }) + .join(", ")), + info.ip_info.as_ref() + .and_then(|ip_info| ip_info.wan_ip) + .map_or_else(|| "N/A".to_owned(), |ip| ip.to_string()) + ]); + } + + table.print_tty(false).unwrap(); + + Ok(()) + }) + .with_about("Show network interfaces StartOS can listen on") + .with_call_remote::(), + ) + .subcommand( + "set-public", + from_fn_async(set_public) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Indicate whether this interface is publicly addressable") + .with_call_remote::(), + ).subcommand( + "unset-public", + from_fn_async(unset_public) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Allow this interface to infer whether it is publicly addressable based on its IPv4 address") + .with_call_remote::(), + ).subcommand("forget", + from_fn_async(forget_iface) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("Forget a disconnected interface") + .with_call_remote::() + ) +} + +async fn list_interfaces( + ctx: RpcContext, +) -> Result, Error> { + Ok(ctx.net_controller.net_iface.ip_info.read()) +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[ts(export)] +struct NetworkInterfaceSetPublicParams { + #[ts(type = "string")] + interface: InternedString, + public: Option, +} + +async fn set_public( + ctx: RpcContext, + NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams, +) -> Result<(), Error> { + ctx.net_controller + .net_iface + .set_public(&interface, Some(public.unwrap_or(true))) + .await +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[ts(export)] +struct UnsetPublicParams { + #[ts(type = "string")] + interface: InternedString, +} + +async fn unset_public( + ctx: RpcContext, + UnsetPublicParams { interface }: UnsetPublicParams, +) -> Result<(), Error> { + ctx.net_controller + .net_iface + .set_public(&interface, None) + .await +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[ts(export)] +struct ForgetInterfaceParams { + #[ts(type = "string")] + interface: InternedString, +} + +async fn forget_iface( + ctx: RpcContext, + ForgetInterfaceParams { interface }: ForgetInterfaceParams, +) -> Result<(), Error> { + ctx.net_controller.net_iface.forget(&interface).await +} + +#[proxy( + interface = "org.freedesktop.NetworkManager", + default_service = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +trait NetworkManager { + #[zbus(property)] + fn all_devices(&self) -> Result, Error>; + + #[zbus(signal)] + fn device_added(&self) -> Result<(), Error>; + + #[zbus(signal)] + fn device_removed(&self) -> Result<(), Error>; + + #[zbus(signal)] + fn state_changed(&self) -> Result<(), Error>; +} + +mod active_connection { + use zbus::proxy; + use zbus::zvariant::OwnedObjectPath; + + use crate::prelude::*; + + #[proxy( + interface = "org.freedesktop.NetworkManager.Connection.Active", + default_service = "org.freedesktop.NetworkManager" + )] + pub trait ActiveConnection { + #[zbus(property)] + fn state_flags(&self) -> Result; + + #[zbus(property, name = "Type")] + fn connection_type(&self) -> Result; + + #[zbus(signal)] + fn state_changed(&self) -> Result<(), Error>; + + #[zbus(property)] + fn dhcp4_config(&self) -> Result; + } +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.IP4Config", + default_service = "org.freedesktop.NetworkManager" +)] +trait Ip4Config { + #[zbus(property)] + fn address_data(&self) -> Result, Error>; +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.IP6Config", + default_service = "org.freedesktop.NetworkManager" +)] +trait Ip6Config { + #[zbus(property)] + fn address_data(&self) -> Result, Error>; +} + +#[derive(Clone, Debug, DeserializeDict, ZValue, ZType)] +#[zvariant(signature = "dict")] +struct AddressData { + address: String, + prefix: u32, +} +impl TryFrom for IpNet { + type Error = Error; + fn try_from(value: AddressData) -> Result { + IpNet::new(value.address.parse()?, value.prefix as u8).with_kind(ErrorKind::ParseNetAddress) + } +} + +#[proxy( + interface = "org.freedesktop.NetworkManager.DHCP4Config", + default_service = "org.freedesktop.NetworkManager" +)] +trait Dhcp4Config { + #[zbus(property)] + fn options(&self) -> Result; +} + +#[derive(Clone, Debug, DeserializeDict, ZType)] +#[zvariant(signature = "dict")] +struct Dhcp4Options { + ntp_servers: Option, +} +impl TryFrom for Dhcp4Options { + type Error = zbus::Error; + fn try_from(value: OwnedValue) -> Result { + let dict = value.downcast_ref::()?; + Ok(Self { + ntp_servers: dict.get::<_, String>(&zbus::zvariant::Str::from_static("ntp_servers"))?, + }) + } +} + +mod device { + use zbus::proxy; + use zbus::zvariant::OwnedObjectPath; + + use crate::prelude::*; + + #[proxy( + interface = "org.freedesktop.NetworkManager.Device", + default_service = "org.freedesktop.NetworkManager" + )] + pub trait Device { + #[zbus(property)] + fn ip_interface(&self) -> Result; + + #[zbus(property)] + fn managed(&self) -> Result; + + #[zbus(property)] + fn active_connection(&self) -> Result; + + #[zbus(property)] + fn ip4_config(&self) -> Result; + + #[zbus(property)] + fn ip6_config(&self) -> Result; + + #[zbus(property, name = "State")] + fn _state(&self) -> Result; + + #[zbus(property)] + fn device_type(&self) -> Result; + + #[zbus(signal)] + fn state_changed(&self) -> Result<(), Error>; + } +} + +trait StubStream<'a> { + fn stub(self) -> impl Stream> + 'a; +} +impl<'a, T> StubStream<'a> for PropertyStream<'a, T> +where + T: Unpin + TryFrom + std::fmt::Debug + 'a, + T::Error: Into, +{ + fn stub(self) -> impl Stream> + 'a { + StreamExt::then(self, |d| async move { + PropertyChanged::get(&d).await.map(|_| ()) + }) + .map_err(Error::from) + } +} +impl<'a> StubStream<'a> for SignalStream<'a> { + fn stub(self) -> impl Stream> + 'a { + self.map(|_| Ok(())) + } +} + +#[instrument(skip_all)] +async fn watcher( + write_to: Watch>, + lxcbr_status: Watch, +) { + loop { + let res: Result<(), Error> = async { + let connection = Connection::system().await?; + + let netman_proxy = NetworkManagerProxy::new(&connection).await?; + + let mut until = Until::new() + .with_stream(netman_proxy.receive_all_devices_changed().await.stub()) + .with_stream( + netman_proxy + .receive_device_added() + .await? + .into_inner() + .stub(), + ) + .with_stream( + netman_proxy + .receive_device_removed() + .await? + .into_inner() + .stub(), + ) + .with_stream( + netman_proxy + .receive_state_changed() + .await? + .into_inner() + .stub(), + ); + + loop { + until + .run(async { + let devices = netman_proxy.all_devices().await?; + let mut ifaces = BTreeSet::new(); + let mut jobs = Vec::new(); + for device in devices { + use futures::future::Either; + + let device_proxy = + device::DeviceProxy::new(&connection, device.clone()).await?; + let iface = InternedString::intern(device_proxy.ip_interface().await?); + if iface.is_empty() { + continue; + } else if &*iface == START9_BRIDGE_IFACE { + jobs.push(Either::Left(watch_activated( + &connection, + device_proxy.clone(), + &lxcbr_status, + ))); + } + + jobs.push(Either::Right(watch_ip( + &connection, + device_proxy.clone(), + iface.clone(), + &write_to, + ))); + ifaces.insert(iface); + } + + write_to.send_if_modified(|m| { + let mut changed = false; + for (iface, info) in m { + if !ifaces.contains(iface) { + info.ip_info = None; + changed = true; + } + } + changed + }); + futures::future::try_join_all(jobs).await?; + + Ok::<_, Error>(()) + }) + .await?; + } + } + .await; + if let Err(e) = res { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + } +} + +async fn get_wan_ipv4(iface: &str) -> Result, Error> { + let client = reqwest::Client::builder(); + #[cfg(target_os = "linux")] + let client = client.interface(iface); + Ok(client + .build()? + .get("http://ip4only.me/api/") + .timeout(Duration::from_secs(10)) + .send() + .await? + .error_for_status()? + .text() + .await? + .split(",") + .skip(1) + .next() + .filter(|s| !s.is_empty()) + .map(|s| s.parse()) + .transpose()?) +} + +#[instrument(skip(connection, device_proxy, write_to))] +async fn watch_ip( + connection: &Connection, + device_proxy: device::DeviceProxy<'_>, + iface: InternedString, + write_to: &Watch>, +) -> Result<(), Error> { + let mut until = Until::new() + .with_stream( + device_proxy + .receive_active_connection_changed() + .await + .stub(), + ) + .with_stream( + device_proxy + .receive_state_changed() + .await? + .into_inner() + .stub(), + ) + .with_stream(device_proxy.receive_ip4_config_changed().await.stub()) + .with_stream(device_proxy.receive_ip6_config_changed().await.stub()) + .with_async_fn(|| { + async { + tokio::time::sleep(Duration::from_secs(300)).await; + Ok(()) + } + .fuse() + }); + + loop { + until + .run(async { + let ip4_config = device_proxy.ip4_config().await?; + let ip6_config = device_proxy.ip6_config().await?; + + let managed = device_proxy.managed().await?; + if !managed { + return Ok(()); + } + let dac = device_proxy.active_connection().await?; + if &*dac == "/" { + return Ok(()); + } + + let active_connection_proxy = + active_connection::ActiveConnectionProxy::new(&connection, dac).await?; + + let mut until = Until::new() + .with_stream( + active_connection_proxy + .receive_state_changed() + .await? + .into_inner() + .stub(), + ) + .with_stream( + active_connection_proxy + .receive_dhcp4_config_changed() + .await + .stub(), + ); + + loop { + until + .run(async { + let external = active_connection_proxy.state_flags().await? & 0x80 != 0; + if external { + return Ok(()); + } + + let device_type = match device_proxy.device_type().await? { + 1 => Some(NetworkInterfaceType::Ethernet), + 2 => Some(NetworkInterfaceType::Wireless), + 29 => Some(NetworkInterfaceType::Wireguard), + _ => None, + }; + + let dhcp4_config = active_connection_proxy.dhcp4_config().await?; + let ip4_proxy = + Ip4ConfigProxy::new(&connection, ip4_config.clone()).await?; + let ip6_proxy = + Ip6ConfigProxy::new(&connection, ip6_config.clone()).await?; + let mut until = Until::new() + .with_stream(ip4_proxy.receive_address_data_changed().await.stub()) + .with_stream(ip6_proxy.receive_address_data_changed().await.stub()); + + let dhcp4_proxy = if &*dhcp4_config != "/" { + let dhcp4_proxy = + Dhcp4ConfigProxy::new(&connection, dhcp4_config).await?; + until = until.with_stream( + dhcp4_proxy.receive_options_changed().await.stub(), + ); + Some(dhcp4_proxy) + } else { + None + }; + + loop { + until + .run(async { + let addresses = ip4_proxy + .address_data() + .await? + .into_iter() + .chain(ip6_proxy.address_data().await?) + .collect_vec(); + let mut ntp_servers = BTreeSet::new(); + if let Some(dhcp4_proxy) = &dhcp4_proxy { + let dhcp = dhcp4_proxy.options().await?; + if let Some(ntp) = dhcp.ntp_servers { + ntp_servers.extend( + ntp.split_whitespace() + .map(InternedString::intern), + ); + } + } + let scope_id = if_nametoindex(&*iface) + .with_kind(ErrorKind::Network)?; + let subnets: BTreeSet = addresses + .into_iter() + .map(TryInto::try_into) + .try_collect()?; + let ip_info = if !subnets.is_empty() { + let wan_ip = match get_wan_ipv4(&*iface).await { + Ok(a) => a, + Err(e) => { + tracing::error!( + "Failed to determine WAN IP for {iface}: {e}" + ); + tracing::debug!("{e:?}"); + None + } + }; + Some(IpInfo { + scope_id, + device_type, + subnets, + wan_ip, + ntp_servers, + }) + } else { + None + }; + + write_to.send_if_modified(|m| { + let public = m.get(&iface).map_or(None, |i| i.public); + m.insert( + iface.clone(), + NetworkInterfaceInfo { + public, + ip_info: ip_info.clone(), + }, + ) + .filter(|old| &old.ip_info == &ip_info) + .is_none() + }); + + Ok::<_, Error>(()) + }) + .await?; + } + }) + .await?; + } + }) + .await?; + } +} + +#[instrument(skip(_connection, device_proxy, write_to))] +async fn watch_activated( + _connection: &Connection, + device_proxy: device::DeviceProxy<'_>, + write_to: &Watch, +) -> Result<(), Error> { + let mut until = Until::new() + .with_stream( + device_proxy + .receive_active_connection_changed() + .await + .stub(), + ) + .with_stream( + device_proxy + .receive_state_changed() + .await? + .into_inner() + .stub(), + ); + + loop { + until + .run(async { + write_to.send(device_proxy._state().await? == 100); + Ok(()) + }) + .await?; + } +} + +pub struct NetworkInterfaceController { + db: TypedPatchDb, + lxcbr_status: Watch, + ip_info: Watch>, + _watcher: NonDetachingJoinHandle<()>, + listeners: SyncMutex>>, +} +impl NetworkInterfaceController { + pub fn lxcbr_status(&self) -> Watch { + self.lxcbr_status.clone_unseen() + } + + pub fn subscribe(&self) -> Watch> { + self.ip_info.clone_unseen() + } + + pub fn ip_info(&self) -> BTreeMap { + self.ip_info.read() + } + + async fn sync( + db: &TypedPatchDb, + info: &BTreeMap, + ) -> Result<(), Error> { + tracing::debug!("syncronizing {info:?} to db"); + + db.mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_network_interfaces_mut() + .ser(info) + }) + .await?; + + let ntp: BTreeSet<_> = info + .values() + .filter_map(|i| i.ip_info.as_ref()) + .flat_map(|i| &i.ntp_servers) + .cloned() + .collect(); + let prev_ntp = tokio_stream::wrappers::LinesStream::new( + BufReader::new(open_file("/etc/systemd/timesyncd.conf").await?).lines(), + ) + .try_filter_map(|l| async move { + Ok(l.strip_prefix("NTP=").map(|s| { + s.split_whitespace() + .map(InternedString::intern) + .collect::>() + })) + }) + .boxed() + .try_next() + .await? + .unwrap_or_default(); + if ntp != prev_ntp { + // sed -i '/\(^\|#\)NTP=/c\NTP='"${servers}" /etc/systemd/timesyncd.conf + Command::new("sed") + .arg("-i") + .arg( + [r#"/\(^\|#\)NTP=/c\NTP="#] + .into_iter() + .chain(Itertools::intersperse( + { + fn to_str(ntp: &InternedString) -> &str { + &*ntp + } + ntp.iter().map(to_str) + }, + " ", + )) + .join(""), + ) + .arg("/etc/systemd/timesyncd.conf") + .invoke(ErrorKind::Filesystem) + .await?; + Command::new("systemctl") + .arg("restart") + .arg("systemd-timesyncd") + .invoke(ErrorKind::Systemd) + .await?; + } + + Ok(()) + } + pub fn new(db: TypedPatchDb) -> Self { + let mut ip_info = Watch::new(BTreeMap::new()); + let lxcbr_status = Watch::new(false); + Self { + db: db.clone(), + lxcbr_status: lxcbr_status.clone(), + ip_info: ip_info.clone(), + _watcher: tokio::spawn(async move { + match db + .peek() + .await + .as_public() + .as_server_info() + .as_network_interfaces() + .de() + { + Ok(mut info) => { + for info in info.values_mut() { + info.ip_info = None; + } + ip_info.send_replace(info); + } + Err(e) => { + tracing::error!("Error loading network interface info: {e}"); + tracing::debug!("{e:?}"); + } + }; + tokio::join!(watcher(ip_info.clone(), lxcbr_status), async { + let res: Result<(), Error> = async { + loop { + if let Err(e) = async { + let ip_info = ip_info.read(); + Self::sync(&db, &ip_info).boxed().await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error syncing ip info to db: {e}"); + tracing::debug!("{e:?}"); + } + + let _ = ip_info.changed().await; + } + } + .await; + if let Err(e) = res { + tracing::error!("Error syncing ip info to db: {e}"); + tracing::debug!("{e:?}"); + } + }); + }) + .into(), + listeners: SyncMutex::new(BTreeMap::new()), + } + } + + pub fn bind(&self, port: u16) -> Result { + let arc = Arc::new(()); + self.listeners.mutate(|l| { + if l.get(&port).filter(|w| w.strong_count() > 0).is_some() { + return Err(Error::new( + std::io::Error::from_raw_os_error(libc::EADDRINUSE), + ErrorKind::Network, + )); + } + l.insert(port, Arc::downgrade(&arc)); + Ok(()) + })?; + let ip_info = self.ip_info.clone_unseen(); + Ok(NetworkInterfaceListener { + _arc: arc, + ip_info, + listeners: ListenerMap::new(port), + }) + } + + pub fn upgrade_listener( + &self, + SelfContainedNetworkInterfaceListener { + mut listener, + .. + }: SelfContainedNetworkInterfaceListener, + ) -> Result { + let port = listener.listeners.port; + let arc = &listener._arc; + self.listeners.mutate(|l| { + if l.get(&port).filter(|w| w.strong_count() > 0).is_some() { + return Err(Error::new( + std::io::Error::from_raw_os_error(libc::EADDRINUSE), + ErrorKind::Network, + )); + } + l.insert(port, Arc::downgrade(arc)); + Ok(()) + })?; + let ip_info = self.ip_info.clone_unseen(); + ip_info.mark_changed(); + listener.change_ip_info_source(ip_info); + Ok(listener) + } + + pub async fn set_public( + &self, + interface: &InternedString, + public: Option, + ) -> Result<(), Error> { + let mut sub = self + .db + .subscribe( + "/public/serverInfo/networkInterfaces" + .parse::>() + .with_kind(ErrorKind::Database)?, + ) + .await; + let mut err = None; + let changed = self.ip_info.send_if_modified(|ip_info| { + let prev = std::mem::replace( + &mut match ip_info.get_mut(interface).or_not_found(interface) { + Ok(a) => a, + Err(e) => { + err = Some(e); + return false; + } + } + .public, + public, + ); + prev != public + }); + if let Some(e) = err { + return Err(e); + } + if changed { + sub.recv().await; + } + Ok(()) + } + + pub async fn forget(&self, interface: &InternedString) -> Result<(), Error> { + let mut sub = self + .db + .subscribe( + "/public/serverInfo/networkInterfaces" + .parse::>() + .with_kind(ErrorKind::Database)?, + ) + .await; + let mut err = None; + let changed = self.ip_info.send_if_modified(|ip_info| { + if ip_info + .get(interface) + .map_or(false, |i| i.ip_info.is_some()) + { + err = Some(Error::new( + eyre!("Cannot forget currently connected interface"), + ErrorKind::InvalidRequest, + )); + return false; + } + ip_info.remove(interface).is_some() + }); + if let Some(e) = err { + return Err(e); + } + if changed { + sub.recv().await; + } + Ok(()) + } +} + +struct ListenerMap { + prev_public: bool, + port: u16, + listeners: BTreeMap)>, +} +impl ListenerMap { + fn from_listener(listener: impl IntoIterator) -> Result { + let mut prev_public = false; + let mut port = 0; + let mut listeners = BTreeMap::)>::new(); + for listener in listener { + let mut local = listener.local_addr().with_kind(ErrorKind::Network)?; + if let SocketAddr::V6(l) = &mut local { + if ipv6_is_link_local(*l.ip()) && l.scope_id() == 0 { + continue; // TODO determine scope id + } + } + if port != 0 && port != local.port() { + return Err(Error::new( + eyre!("Provided listeners are bound to different ports"), + ErrorKind::InvalidRequest, + )); + } + let public = match local.ip() { + IpAddr::V4(ip4) => { + !ip4.is_loopback() + && (!ip4.is_private() || ip4.octets().starts_with(&[10, 59])) // reserving 10.59 for public wireguard configurations + && !ip4.is_link_local() + } + IpAddr::V6(ip6) => !ipv6_is_local(ip6), + }; + prev_public |= public; + port = local.port(); + listeners.insert(local, (listener, public, None)); + } + if port == 0 { + return Err(Error::new( + eyre!("Listener array cannot be empty"), + ErrorKind::InvalidRequest, + )); + } + Ok(Self { + prev_public, + port, + listeners, + }) + } +} +impl ListenerMap { + fn new(port: u16) -> Self { + Self { + prev_public: false, + port, + listeners: BTreeMap::new(), + } + } + + #[instrument(skip(self))] + fn update( + &mut self, + ip_info: &BTreeMap, + public: bool, + ) -> Result<(), Error> { + let mut keep = BTreeSet::::new(); + for info in ip_info.values().chain([&NetworkInterfaceInfo { + public: Some(false), + ip_info: Some(IpInfo { + scope_id: 1, + device_type: None, + subnets: [ + IpNet::new(Ipv4Addr::LOCALHOST.into(), 8).unwrap(), + IpNet::new(Ipv6Addr::LOCALHOST.into(), 128).unwrap(), + ] + .into_iter() + .collect(), + wan_ip: None, + ntp_servers: Default::default(), + }), + }]) { + if public || !info.public() { + if let Some(ip_info) = &info.ip_info { + for ipnet in &ip_info.subnets { + let addr = match ipnet.addr() { + IpAddr::V6(ip6) => SocketAddrV6::new( + ip6, + self.port, + 0, + if ipv6_is_link_local(ip6) { + ip_info.scope_id + } else { + 0 + }, + ) + .into(), + ip => SocketAddr::new(ip, self.port), + }; + keep.insert(addr); + if let Some((_, is_public, wan_ip)) = self.listeners.get_mut(&addr) { + *is_public = info.public(); + *wan_ip = info.ip_info.as_ref().and_then(|i| i.wan_ip); + continue; + } + self.listeners.insert( + addr, + ( + TcpListener::from_std( + mio::net::TcpListener::bind(addr) + .with_ctx(|_| { + ( + ErrorKind::Network, + lazy_format!("binding to {addr:?}"), + ) + })? + .into(), + ) + .with_kind(ErrorKind::Network)?, + info.public(), + info.ip_info.as_ref().and_then(|i| i.wan_ip), + ), + ); + } + } + } + } + self.listeners.retain(|key, _| keep.contains(key)); + self.prev_public = public; + Ok(()) + } + fn poll_accept(&self, cx: &mut std::task::Context<'_>) -> Poll> { + for (bind_addr, listener) in self.listeners.iter() { + if let Poll::Ready((stream, addr)) = listener.0.poll_accept(cx)? { + return Poll::Ready(Ok(Accepted { + stream, + peer: addr, + is_public: listener.1, + wan_ip: listener.2, + bind: *bind_addr, + })); + } + } + Poll::Pending + } +} + +pub struct NetworkInterfaceListener { + ip_info: Watch>, + listeners: ListenerMap, + _arc: Arc<()>, +} +impl NetworkInterfaceListener { + pub fn port(&self) -> u16 { + self.listeners.port + } + + pub fn poll_accept( + &mut self, + cx: &mut std::task::Context<'_>, + public: bool, + ) -> Poll> { + while self.ip_info.poll_changed(cx).is_ready() || public != self.listeners.prev_public { + self.ip_info + .peek(|ip_info| self.listeners.update(ip_info, public))?; + } + self.listeners.poll_accept(cx) + } + + pub(super) fn new( + mut ip_info: Watch>, + port: u16, + ) -> Self { + ip_info.mark_unseen(); + Self { + ip_info, + listeners: ListenerMap::new(port), + _arc: Arc::new(()), + } + } + + pub fn change_ip_info_source( + &mut self, + mut ip_info: Watch>, + ) { + ip_info.mark_unseen(); + self.ip_info = ip_info; + } + + pub async fn accept(&mut self, public: bool) -> Result { + futures::future::poll_fn(|cx| self.poll_accept(cx, public)).await + } +} + +pub struct Accepted { + pub stream: TcpStream, + pub peer: SocketAddr, + pub is_public: bool, + pub wan_ip: Option, + pub bind: SocketAddr, +} + +pub struct SelfContainedNetworkInterfaceListener { + _watch_thread: NonDetachingJoinHandle<()>, + listener: NetworkInterfaceListener, +} +impl SelfContainedNetworkInterfaceListener { + pub fn bind(port: u16) -> Self { + let ip_info = Watch::new(BTreeMap::new()); + let _watch_thread = tokio::spawn(watcher(ip_info.clone(), Watch::new(false))).into(); + Self { + _watch_thread, + listener: NetworkInterfaceListener::new(ip_info, port), + } + } +} +impl Accept for SelfContainedNetworkInterfaceListener { + fn poll_accept( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Accept::poll_accept(&mut self.listener, cx) + } +} diff --git a/core/startos/src/net/refresher.html b/core/startos/src/net/refresher.html new file mode 100644 index 000000000..445c6b5be --- /dev/null +++ b/core/startos/src/net/refresher.html @@ -0,0 +1,11 @@ + + + StartOS: Loading... + + + + Loading... + + \ No newline at end of file diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs new file mode 100644 index 000000000..ad2900da7 --- /dev/null +++ b/core/startos/src/net/service_interface.rs @@ -0,0 +1,102 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use imbl_value::InternedString; +use models::{HostId, ServiceInterfaceId}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] +#[serde(tag = "kind")] +pub enum HostnameInfo { + Ip { + #[ts(type = "string")] + network_interface_id: InternedString, + public: bool, + hostname: IpHostname, + }, + Onion { + hostname: OnionHostname, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct OnionHostname { + pub value: String, + pub port: Option, + pub ssl_port: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] +#[serde(tag = "kind")] +pub enum IpHostname { + Ipv4 { + value: Ipv4Addr, + port: Option, + ssl_port: Option, + }, + Ipv6 { + value: Ipv6Addr, + #[serde(default)] + scope_id: u32, + port: Option, + ssl_port: Option, + }, + Local { + #[ts(type = "string")] + value: InternedString, + port: Option, + ssl_port: Option, + }, + Domain { + #[ts(type = "string")] + domain: InternedString, + #[ts(type = "string | null")] + subdomain: Option, + port: Option, + ssl_port: Option, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ServiceInterface { + pub id: ServiceInterfaceId, + pub name: String, + pub description: String, + pub masked: bool, + pub address_info: AddressInfo, + #[serde(rename = "type")] + pub interface_type: ServiceInterfaceType, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub enum ServiceInterfaceType { + Ui, + P2p, + Api, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct AddressInfo { + pub username: Option, + pub host_id: HostId, + pub internal_port: u16, + #[ts(type = "string | null")] + pub scheme: Option, + #[ts(type = "string | null")] + pub ssl_scheme: Option, + pub suffix: String, +} diff --git a/core/startos/src/net/ssl.rs b/core/startos/src/net/ssl.rs index 1f9397add..a89853591 100644 --- a/core/startos/src/net/ssl.rs +++ b/core/startos/src/net/ssl.rs @@ -1,12 +1,13 @@ -use std::cmp::Ordering; +use std::cmp::{min, Ordering}; use std::collections::{BTreeMap, BTreeSet}; use std::net::IpAddr; use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use futures::FutureExt; +use imbl_value::InternedString; use libc::time_t; -use openssl::asn1::{Asn1Integer, Asn1Time}; +use openssl::asn1::{Asn1Integer, Asn1Time, Asn1TimeRef}; use openssl::bn::{BigNum, MsbOption}; use openssl::ec::{EcGroup, EcKey}; use openssl::hash::MessageDigest; @@ -14,17 +15,152 @@ use openssl::nid::Nid; use openssl::pkey::{PKey, Private}; use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; use openssl::*; -use rpc_toolkit::command; -use tokio::sync::{Mutex, RwLock}; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; use tracing::instrument; use crate::account::AccountInfo; -use crate::context::RpcContext; use crate::hostname::Hostname; use crate::init::check_time_is_synchronized; -use crate::net::dhcp::ips; -use crate::net::keys::{Key, KeyInfo}; -use crate::{Error, ErrorKind, ResultExt, SOURCE_DATE}; +use crate::prelude::*; +use crate::util::serde::Pem; +use crate::SOURCE_DATE; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[model = "Model"] +#[serde(rename_all = "camelCase")] +pub struct CertStore { + pub root_key: Pem>, + pub root_cert: Pem, + pub int_key: Pem>, + pub int_cert: Pem, + pub leaves: BTreeMap>, CertData>, +} +impl CertStore { + pub fn new(account: &AccountInfo) -> Result { + let int_key = generate_key()?; + let int_cert = make_int_cert((&account.root_ca_key, &account.root_ca_cert), &int_key)?; + Ok(Self { + root_key: Pem::new(account.root_ca_key.clone()), + root_cert: Pem::new(account.root_ca_cert.clone()), + int_key: Pem::new(int_key), + int_cert: Pem::new(int_cert), + leaves: BTreeMap::new(), + }) + } +} +impl Model { + /// This function will grant any cert for any domain. It is up to the *caller* to enusure that the calling service has permission to sign a cert for the requested domain + pub fn cert_for( + &mut self, + hostnames: &BTreeSet, + ) -> Result { + let keys = if let Some(cert_data) = self + .as_leaves() + .as_idx(JsonKey::new_ref(hostnames)) + .map(|m| m.de()) + .transpose()? + { + if cert_data + .certs + .ed25519 + .not_before() + .compare(Asn1Time::days_from_now(0)?.as_ref())? + == Ordering::Less + && cert_data + .certs + .ed25519 + .not_after() + .compare(Asn1Time::days_from_now(30)?.as_ref())? + == Ordering::Greater + && cert_data + .certs + .nistp256 + .not_before() + .compare(Asn1Time::days_from_now(0)?.as_ref())? + == Ordering::Less + && cert_data + .certs + .nistp256 + .not_after() + .compare(Asn1Time::days_from_now(30)?.as_ref())? + == Ordering::Greater + { + return Ok(FullchainCertData { + root: self.as_root_cert().de()?.0, + int: self.as_int_cert().de()?.0, + leaf: cert_data, + }); + } + cert_data.keys + } else { + PKeyPair { + ed25519: PKey::generate_ed25519()?, + nistp256: PKey::from_ec_key(EcKey::generate(&*EcGroup::from_curve_name( + Nid::X9_62_PRIME256V1, + )?)?)?, + } + }; + let int_key = self.as_int_key().de()?.0; + let int_cert = self.as_int_cert().de()?.0; + let cert_data = CertData { + certs: CertPair { + ed25519: make_leaf_cert( + (&int_key, &int_cert), + (&keys.ed25519, &SANInfo::new(hostnames)), + )?, + nistp256: make_leaf_cert( + (&int_key, &int_cert), + (&keys.nistp256, &SANInfo::new(hostnames)), + )?, + }, + keys, + }; + self.as_leaves_mut() + .insert(JsonKey::new_ref(hostnames), &cert_data)?; + Ok(FullchainCertData { + root: self.as_root_cert().de()?.0, + int: self.as_int_cert().de()?.0, + leaf: cert_data, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct CertData { + pub keys: PKeyPair, + pub certs: CertPair, +} +impl CertData { + pub fn expiration(&self) -> Result { + self.certs.expiration() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FullchainCertData { + pub root: X509, + pub int: X509, + pub leaf: CertData, +} +impl FullchainCertData { + pub fn fullchain_ed25519(&self) -> Vec<&X509> { + vec![&self.leaf.certs.ed25519, &self.int, &self.root] + } + pub fn fullchain_nistp256(&self) -> Vec<&X509> { + vec![&self.leaf.certs.nistp256, &self.int, &self.root] + } + pub fn expiration(&self) -> Result { + [ + asn1_time_to_system_time(self.root.not_after())?, + asn1_time_to_system_time(self.int.not_after())?, + self.leaf.expiration()?, + ] + .into_iter() + .min() + .ok_or_else(|| Error::new(eyre!("unreachable"), ErrorKind::Unknown)) + } +} static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. @@ -35,60 +171,52 @@ fn unix_time(time: SystemTime) -> time_t { .unwrap_or_default() } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +lazy_static::lazy_static! { + static ref ASN1_UNIX_EPOCH: Asn1Time = Asn1Time::from_unix(0).unwrap(); +} + +fn asn1_time_to_system_time(time: &Asn1TimeRef) -> Result { + let diff = time.diff(&**ASN1_UNIX_EPOCH)?; + let mut res = UNIX_EPOCH; + if diff.days >= 0 { + res += Duration::from_secs(diff.days as u64 * 86400); + } else { + res -= Duration::from_secs((-1 * diff.days) as u64 * 86400); + } + if diff.secs >= 0 { + res += Duration::from_secs(diff.secs as u64); + } else { + res -= Duration::from_secs((-1 * diff.secs) as u64); + } + Ok(res) +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PKeyPair { + #[serde(with = "crate::util::serde::pem")] + pub ed25519: PKey, + #[serde(with = "crate::util::serde::pem")] + pub nistp256: PKey, +} +impl PartialEq for PKeyPair { + fn eq(&self, other: &Self) -> bool { + self.ed25519.public_eq(&other.ed25519) && self.nistp256.public_eq(&other.nistp256) + } +} +impl Eq for PKeyPair {} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] pub struct CertPair { + #[serde(with = "crate::util::serde::pem")] pub ed25519: X509, + #[serde(with = "crate::util::serde::pem")] pub nistp256: X509, } impl CertPair { - fn updated( - pair: Option<&Self>, - hostname: &Hostname, - signer: (&PKey, &X509), - applicant: &Key, - ip: BTreeSet, - ) -> Result<(Self, bool), Error> { - let mut updated = false; - let mut updated_cert = |cert: Option<&X509>, osk: PKey| -> Result { - let mut ips = BTreeSet::new(); - if let Some(cert) = cert { - ips.extend( - cert.subject_alt_names() - .iter() - .flatten() - .filter_map(|a| a.ipaddress()) - .filter_map(|a| match a.len() { - 4 => Some::(<[u8; 4]>::try_from(a).unwrap().into()), - 16 => Some::(<[u8; 16]>::try_from(a).unwrap().into()), - _ => None, - }), - ); - if cert - .not_before() - .compare(Asn1Time::days_from_now(0)?.as_ref())? - == Ordering::Less - && cert - .not_after() - .compare(Asn1Time::days_from_now(30)?.as_ref())? - == Ordering::Greater - && ips.is_superset(&ip) - { - return Ok(cert.clone()); - } - } - ips.extend(ip.iter().copied()); - updated = true; - make_leaf_cert(signer, (&osk, &SANInfo::new(&applicant, hostname, ips))) - }; - Ok(( - Self { - ed25519: updated_cert(pair.map(|c| &c.ed25519), applicant.openssl_key_ed25519())?, - nistp256: updated_cert( - pair.map(|c| &c.nistp256), - applicant.openssl_key_nistp256(), - )?, - }, - updated, + pub fn expiration(&self) -> Result { + Ok(min( + asn1_time_to_system_time(self.ed25519.not_after())?, + asn1_time_to_system_time(self.nistp256.not_after())?, )) } } @@ -101,55 +229,9 @@ pub async fn root_ca_start_time() -> Result { }) } -#[derive(Debug)] -pub struct SslManager { - hostname: Hostname, - root_cert: X509, - int_key: PKey, - int_cert: X509, - cert_cache: RwLock>, -} -impl SslManager { - pub fn new(account: &AccountInfo, start_time: SystemTime) -> Result { - let int_key = generate_key()?; - let int_cert = make_int_cert( - (&account.root_ca_key, &account.root_ca_cert), - &int_key, - start_time, - )?; - Ok(Self { - hostname: account.hostname.clone(), - root_cert: account.root_ca_cert.clone(), - int_key, - int_cert, - cert_cache: RwLock::new(BTreeMap::new()), - }) - } - pub async fn with_certs(&self, key: Key, ip: IpAddr) -> Result { - let mut ips = ips().await?; - ips.insert(ip); - let (pair, updated) = CertPair::updated( - self.cert_cache.read().await.get(&key), - &self.hostname, - (&self.int_key, &self.int_cert), - &key, - ips, - )?; - if updated { - self.cert_cache - .write() - .await - .insert(key.clone(), pair.clone()); - } - - Ok(key.with_certs(pair, self.int_cert.clone(), self.root_cert.clone())) - } -} - const EC_CURVE_NAME: nid::Nid = nid::Nid::X9_62_PRIME256V1; lazy_static::lazy_static! { static ref EC_GROUP: EcGroup = EcGroup::from_curve_name(EC_CURVE_NAME).unwrap(); - static ref SSL_MUTEX: Mutex<()> = Mutex::new(()); // TODO: make thread safe } pub async fn export_key(key: &PKey, target: &Path) -> Result<(), Error> { @@ -245,18 +327,13 @@ pub fn make_root_cert( pub fn make_int_cert( signer: (&PKey, &X509), applicant: &PKey, - start_time: SystemTime, ) -> Result { let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let unix_start_time = unix_time(start_time); - - let embargo = Asn1Time::from_unix(unix_start_time - 86400)?; - builder.set_not_before(&embargo)?; + builder.set_not_before(signer.1.not_before())?; - let expiration = Asn1Time::from_unix(unix_start_time + (10 * 364 * 86400))?; - builder.set_not_after(&expiration)?; + builder.set_not_after(signer.1.not_after())?; builder.set_serial_number(&*rand_serial()?)?; @@ -309,13 +386,13 @@ pub fn make_int_cert( #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum MaybeWildcard { WithWildcard(String), - WithoutWildcard(String), + WithoutWildcard(InternedString), } impl MaybeWildcard { pub fn as_str(&self) -> &str { match self { MaybeWildcard::WithWildcard(s) => s.as_str(), - MaybeWildcard::WithoutWildcard(s) => s.as_str(), + MaybeWildcard::WithoutWildcard(s) => &**s, } } } @@ -334,18 +411,16 @@ pub struct SANInfo { pub ips: BTreeSet, } impl SANInfo { - pub fn new(key: &Key, hostname: &Hostname, ips: BTreeSet) -> Self { + pub fn new(hostnames: &BTreeSet) -> Self { let mut dns = BTreeSet::new(); - if let Some((id, _)) = key.interface() { - dns.insert(MaybeWildcard::WithWildcard(format!("{id}.embassy"))); - dns.insert(MaybeWildcard::WithWildcard(key.local_address().to_string())); - } else { - dns.insert(MaybeWildcard::WithoutWildcard("embassy".to_owned())); - dns.insert(MaybeWildcard::WithWildcard(hostname.local_domain_name())); - dns.insert(MaybeWildcard::WithoutWildcard(hostname.no_dot_host_name())); - dns.insert(MaybeWildcard::WithoutWildcard("localhost".to_owned())); + let mut ips = BTreeSet::new(); + for hostname in hostnames { + if let Ok(ip) = hostname.parse::() { + ips.insert(ip); + } else { + dns.insert(MaybeWildcard::WithoutWildcard(hostname.clone())); // TODO: wildcards? + } } - dns.insert(MaybeWildcard::WithWildcard(key.tor_address().to_string())); Self { dns, ips } } } @@ -443,16 +518,3 @@ pub fn make_leaf_cert( let cert = builder.build(); Ok(cert) } - -#[command(subcommands(size))] -pub async fn ssl() -> Result<(), Error> { - Ok(()) -} - -#[command] -pub async fn size(#[context] ctx: RpcContext) -> Result { - Ok(format!( - "Cert Catch size: {}", - ctx.net_controller.ssl.cert_cache.read().await.len() - )) -} diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 761566a2c..d1070f9e1 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -1,53 +1,69 @@ -use std::fs::Metadata; +use std::cmp::min; use std::future::Future; +use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::UNIX_EPOCH; use async_compression::tokio::bufread::GzipEncoder; -use color_eyre::eyre::eyre; +use axum::body::Body; +use axum::extract::{self as x, Request}; +use axum::response::{Redirect, Response}; +use axum::routing::{any, get}; +use axum::Router; +use base64::display::Base64Display; use digest::Digest; -use futures::FutureExt; -use http::header::ACCEPT_ENCODING; +use futures::future::ready; +use http::header::{ + ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, + CONTENT_RANGE, CONTENT_TYPE, ETAG, HOST, RANGE, +}; use http::request::Parts as RequestParts; -use hyper::{Body, Method, Request, Response, StatusCode}; -use include_dir::{include_dir, Dir}; +use http::{HeaderValue, Method, StatusCode}; +use imbl_value::InternedString; +use include_dir::Dir; use new_mime_guess::MimeGuess; use openssl::hash::MessageDigest; use openssl::x509::X509; -use rpc_toolkit::rpc_handler; -use tokio::fs::File; -use tokio::io::BufReader; +use rpc_toolkit::{Context, HttpServer, Server}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader}; use tokio_util::io::ReaderStream; +use url::Url; -use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; -use crate::core::rpc_continuations::RequestGuid; -use crate::db::subscribe; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; -use crate::install::PKG_PUBLIC_DIR; -use crate::middleware::auth::{auth as auth_middleware, HasValidSession}; -use crate::middleware::cors::cors; -use crate::middleware::db::db as db_middleware; -use crate::middleware::diagnostic::diagnostic as diagnostic_middleware; -use crate::net::HttpHandler; -use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt}; - -static NOT_FOUND: &[u8] = b"Not Found"; -static METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; -static NOT_AUTHORIZED: &[u8] = b"Not Authorized"; - -static EMBEDDED_UIS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); +use crate::install::PKG_ARCHIVE_DIR; +use crate::middleware::auth::{Auth, HasValidSession}; +use crate::middleware::cors::Cors; +use crate::middleware::db::SyncDb; +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::rpc_continuations::{Guid, RpcContinuations}; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; +use crate::util::io::open_file; +use crate::util::net::SyncBody; +use crate::util::serde::BASE64; +use crate::{diagnostic_api, init_api, install_api, main_api, setup_api, DATA_DIR}; + +const NOT_FOUND: &[u8] = b"Not Found"; +const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; +const NOT_AUTHORIZED: &[u8] = b"Not Authorized"; +const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"]; -fn status_fn(_: i32) -> StatusCode { - StatusCode::OK -} +#[cfg(all(feature = "daemon", not(feature = "test")))] +const EMBEDDED_UIS: Dir<'_> = + include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); +#[cfg(not(all(feature = "daemon", not(feature = "test"))))] +const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]); #[derive(Clone)] pub enum UiMode { Setup, - Diag, Install, Main, } @@ -56,190 +72,49 @@ impl UiMode { fn path(&self, path: &str) -> PathBuf { match self { Self::Setup => Path::new("setup-wizard").join(path), - Self::Diag => Path::new("diagnostic-ui").join(path), Self::Install => Path::new("install-wizard").join(path), Self::Main => Path::new("ui").join(path), } } } -pub async fn setup_ui_file_router(ctx: SetupContext) -> Result { - let handler: HttpHandler = Arc::new(move |req| { - let ctx = ctx.clone(); - - let ui_mode = UiMode::Setup; - async move { - let res = match req.uri().path() { - path if path.starts_with("/rpc/") => { - let rpc_handler = rpc_handler!({ - command: setup_api, - context: ctx, - status: status_fn, - middleware: [ - cors, - ] - }); - - rpc_handler(req) - .await - .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) - } - _ => alt_ui(req, ui_mode).await, - }; - - match res { - Ok(data) => Ok(data), - Err(err) => Ok(server_error(err)), - } - } - .boxed() - }); - - Ok(handler) -} - -pub async fn diag_ui_file_router(ctx: DiagnosticContext) -> Result { - let handler: HttpHandler = Arc::new(move |req| { - let ctx = ctx.clone(); - let ui_mode = UiMode::Diag; - async move { - let res = match req.uri().path() { - path if path.starts_with("/rpc/") => { - let rpc_handler = rpc_handler!({ - command: diagnostic_api, - context: ctx, - status: status_fn, - middleware: [ - cors, - diagnostic_middleware, - ] - }); - - rpc_handler(req) - .await - .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) - } - _ => alt_ui(req, ui_mode).await, - }; - - match res { - Ok(data) => Ok(data), - Err(err) => Ok(server_error(err)), - } - } - .boxed() - }); - - Ok(handler) -} - -pub async fn install_ui_file_router(ctx: InstallContext) -> Result { - let handler: HttpHandler = Arc::new(move |req| { - let ctx = ctx.clone(); - let ui_mode = UiMode::Install; - async move { - let res = match req.uri().path() { - path if path.starts_with("/rpc/") => { - let rpc_handler = rpc_handler!({ - command: install_api, - context: ctx, - status: status_fn, - middleware: [ - cors, - ] - }); - - rpc_handler(req) - .await - .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) - } - _ => alt_ui(req, ui_mode).await, - }; - - match res { - Ok(data) => Ok(data), - Err(err) => Ok(server_error(err)), - } - } - .boxed() - }); - - Ok(handler) -} - -pub async fn main_ui_server_router(ctx: RpcContext) -> Result { - let handler: HttpHandler = Arc::new(move |req| { - let ctx = ctx.clone(); - - async move { - let res = match req.uri().path() { - path if path.starts_with("/rpc/") => { - let auth_middleware = auth_middleware(ctx.clone()); - let db_middleware = db_middleware(ctx.clone()); - let rpc_handler = rpc_handler!({ - command: main_api, - context: ctx, - status: status_fn, - middleware: [ - cors, - auth_middleware, - db_middleware, - ] - }); - - rpc_handler(req) - .await - .map_err(|err| Error::new(eyre!("{}", err), crate::ErrorKind::Network)) - } - "/ws/db" => subscribe(ctx, req).await, - path if path.starts_with("/ws/rpc/") => { - match RequestGuid::from(path.strip_prefix("/ws/rpc/").unwrap()) { - None => { - tracing::debug!("No Guid Path"); - Ok::<_, Error>(bad_request()) - } - Some(guid) => match ctx.get_ws_continuation_handler(&guid).await { - Some(cont) => match cont(req).await { - Ok::<_, Error>(r) => Ok::<_, Error>(r), - Err(err) => Ok::<_, Error>(server_error(err)), - }, - _ => Ok::<_, Error>(not_found()), - }, +pub fn rpc_router>( + ctx: C, + server: HttpServer, +) -> Router { + Router::new() + .route("/rpc/*path", any(server)) + .route( + "/ws/rpc/:guid", + get({ + let ctx = ctx.clone(); + move |x::Path(guid): x::Path, + ws: axum::extract::ws::WebSocketUpgrade| async move { + match AsRef::::as_ref(&ctx).get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), } } - path if path.starts_with("/rest/rpc/") => { - match RequestGuid::from(path.strip_prefix("/rest/rpc/").unwrap()) { - None => { - tracing::debug!("No Guid Path"); - Ok::<_, Error>(bad_request()) - } - Some(guid) => match ctx.get_rest_continuation_handler(&guid).await { - None => Ok::<_, Error>(not_found()), - Some(cont) => match cont(req).await { - Ok::<_, Error>(r) => Ok::<_, Error>(r), - Err(e) => Ok::<_, Error>(server_error(e)), - }, - }, + }), + ) + .route( + "/rest/rpc/:guid", + any({ + let ctx = ctx.clone(); + move |x::Path(guid): x::Path, request: x::Request| async move { + match AsRef::::as_ref(&ctx).get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), } } - _ => main_embassy_ui(req, ctx).await, - }; - - match res { - Ok(data) => Ok(data), - Err(err) => Ok(server_error(err)), - } - } - .boxed() - }); - - Ok(handler) + }), + ) } -async fn alt_ui(req: Request, ui_mode: UiMode) -> Result, Error> { +fn serve_ui(req: Request, ui_mode: UiMode) -> Result { let (request_parts, _body) = req.into_parts(); match &request_parts.method { - &Method::GET => { + &Method::GET | &Method::HEAD => { let uri_path = ui_mode.path( request_parts .uri @@ -253,9 +128,7 @@ async fn alt_ui(req: Request, ui_mode: UiMode) -> Result, E .or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html"))); if let Some(file) = file { - FileData::from_embedded(&request_parts, file) - .into_response(&request_parts) - .await + FileData::from_embedded(&request_parts, file)?.into_response(&request_parts) } else { Ok(not_found()) } @@ -264,121 +137,245 @@ async fn alt_ui(req: Request, ui_mode: UiMode) -> Result, E } } -async fn if_authorized< - F: FnOnce() -> Fut, - Fut: Future, Error>> + Send + Sync, ->( - ctx: &RpcContext, - parts: &RequestParts, - f: F, -) -> Result, Error> { - if let Err(e) = HasValidSession::from_request_parts(parts, ctx).await { - un_authorized(e, parts.uri.path()) - } else { - f().await - } +pub fn setup_ui_router(ctx: SetupContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Setup).unwrap_or_else(server_error) + })) } -async fn main_embassy_ui(req: Request, ctx: RpcContext) -> Result, Error> { - let (request_parts, _body) = req.into_parts(); - match ( - &request_parts.method, - request_parts - .uri - .path() - .strip_prefix('/') - .unwrap_or(request_parts.uri.path()) - .split_once('/'), - ) { - (&Method::GET, Some(("public", path))) => { - if_authorized(&ctx, &request_parts, || async { - let sub_path = Path::new(path); - if let Ok(rest) = sub_path.strip_prefix("package-data") { - FileData::from_path( - &request_parts, - &ctx.datadir.join(PKG_PUBLIC_DIR).join(rest), - ) - .await? - .into_response(&request_parts) - .await - } else { - Ok(not_found()) - } - }) - .await - } - (&Method::GET, Some(("proxy", target))) => { - if_authorized(&ctx, &request_parts, || async { - let target = urlencoding::decode(target)?; - let res = ctx - .client - .get(target.as_ref()) - .headers( - request_parts - .headers - .iter() - .filter(|(h, _)| { - !PROXY_STRIP_HEADERS - .iter() - .any(|bad| h.as_str().eq_ignore_ascii_case(bad)) - }) - .map(|(h, v)| (h.clone(), v.clone())) - .collect(), - ) - .send() - .await - .with_kind(crate::ErrorKind::Network)?; - let mut hres = Response::builder().status(res.status()); - for (h, v) in res.headers().clone() { - if let Some(h) = h { - hres = hres.header(h, v); - } - } - hres.body(Body::wrap_stream(res.bytes_stream())) - .with_kind(crate::ErrorKind::Network) - }) - .await - } - (&Method::GET, Some(("eos", "local.crt"))) => { - let account = ctx.account.read().await; - cert_send(&account.root_ca_cert, &account.hostname) - } - (&Method::GET, _) => { - let uri_path = UiMode::Main.path( - request_parts - .uri - .path() - .strip_prefix('/') - .unwrap_or(request_parts.uri.path()), - ); +pub fn diagnostic_ui_router(ctx: DiagnosticContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} - let file = EMBEDDED_UIS - .get_file(&*uri_path) - .or_else(|| EMBEDDED_UIS.get_file(&*UiMode::Main.path("index.html"))); +pub fn install_ui_router(ctx: InstallContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Install).unwrap_or_else(server_error) + })) +} - if let Some(file) = file { - FileData::from_embedded(&request_parts, file) - .into_response(&request_parts) +pub fn init_ui_router(ctx: InitContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), init_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} + +pub fn main_ui_router(ctx: RpcContext) -> Router { + rpc_router(ctx.clone(), { + let ctx = ctx.clone(); + Server::new(move || ready(Ok(ctx.clone())), main_api::()) + .middleware(Cors::new()) + .middleware(Auth::new()) + .middleware(SyncDb::new()) + }) + .route("/proxy/:url", { + let ctx = ctx.clone(); + any(move |x::Path(url): x::Path, request: Request| { + let ctx = ctx.clone(); + async move { + proxy_request(ctx, request, url) .await - } else { - Ok(not_found()) + .unwrap_or_else(server_error) } + }) + }) + .nest("/s9pk", s9pk_router(ctx.clone())) + .route( + "/static/local-root-ca.crt", + get(move || { + let ctx = ctx.clone(); + async move { + let account = ctx.account.read().await; + cert_send(&account.root_ca_cert, &account.hostname) + } + }), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} + +pub fn refresher() -> Router { + Router::new().fallback(get(|request: Request| async move { + let res = include_bytes!("./refresher.html"); + FileData { + data: Body::from(&res[..]), + content_range: None, + e_tag: None, + encoding: None, + len: Some(res.len() as u64), + mime: Some("text/html".into()), + digest: None, } - _ => Ok(method_not_allowed()), + .into_response(&request.into_parts().0) + .unwrap_or_else(server_error) + })) +} + +pub fn redirecter() -> Router { + Router::new().fallback(get(|request: Request| async move { + Redirect::temporary(&format!( + "https://{}{}", + request + .headers() + .get(HOST) + .and_then(|s| s.to_str().ok()) + .unwrap_or("localhost"), + request.uri() + )) + })) +} + +async fn proxy_request(ctx: RpcContext, request: Request, url: String) -> Result { + if_authorized(&ctx, request, |mut request| async { + for header in PROXY_STRIP_HEADERS { + request.headers_mut().remove(*header); + } + *request.uri_mut() = url.parse()?; + let request = request.map(|b| reqwest::Body::wrap_stream(SyncBody::from(b))); + let response = ctx.client.execute(request.try_into()?).await?; + Ok(Response::from(response).map(|b| Body::new(b))) + }) + .await +} + +fn s9pk_router(ctx: RpcContext) -> Router { + Router::new() + .route("/installed/:s9pk", { + let ctx = ctx.clone(); + any( + |x::Path(s9pk): x::Path, request: Request| async move { + if_authorized(&ctx, request, |request| async { + let (parts, _) = request.into_parts(); + match FileData::from_path( + &parts, + &Path::new(DATA_DIR) + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await? + { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route("/installed/:s9pk/*path", { + let ctx = ctx.clone(); + any( + |x::Path((s9pk, path)): x::Path<(String, PathBuf)>, + x::RawQuery(query): x::RawQuery, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &MultiCursorFile::from( + open_file( + Path::new(DATA_DIR) + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await?, + ), + query + .as_deref() + .map(MerkleArchiveCommitment::from_query) + .and_then(|a| a.transpose()) + .transpose()? + .as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route( + "/proxy/:url/*path", + any( + |x::Path((url, path)): x::Path<(Url, PathBuf)>, + x::RawQuery(query): x::RawQuery, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + query + .as_deref() + .map(MerkleArchiveCommitment::from_query) + .and_then(|a| a.transpose()) + .transpose()? + .as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ), + ) +} + +async fn if_authorized< + F: FnOnce(Request) -> Fut, + Fut: Future> + Send, +>( + ctx: &RpcContext, + request: Request, + f: F, +) -> Result { + if let Err(e) = + HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await + { + Ok(unauthorized(e, request.uri().path())) + } else { + f(request).await } } -fn un_authorized(err: Error, path: &str) -> Result, Error> { +pub fn unauthorized(err: Error, path: &str) -> Response { tracing::warn!("unauthorized for {} @{:?}", err, path); tracing::debug!("{:?}", err); - Ok(Response::builder() + Response::builder() .status(StatusCode::UNAUTHORIZED) .body(NOT_AUTHORIZED.into()) - .unwrap()) + .unwrap() } /// HTTP status code 404 -fn not_found() -> Response { +pub fn not_found() -> Response { Response::builder() .status(StatusCode::NOT_FOUND) .body(NOT_FOUND.into()) @@ -386,35 +383,37 @@ fn not_found() -> Response { } /// HTTP status code 405 -fn method_not_allowed() -> Response { +pub fn method_not_allowed() -> Response { Response::builder() .status(StatusCode::METHOD_NOT_ALLOWED) .body(METHOD_NOT_ALLOWED.into()) .unwrap() } -fn server_error(err: Error) -> Response { +pub fn server_error(err: Error) -> Response { + tracing::error!("internal server error: {}", err); + tracing::debug!("{:?}", err); Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string().into()) + .body(INTERNAL_SERVER_ERROR.into()) .unwrap() } -fn bad_request() -> Response { +pub fn bad_request() -> Response { Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::empty()) .unwrap() } -fn cert_send(cert: &X509, hostname: &Hostname) -> Result, Error> { +fn cert_send(cert: &X509, hostname: &Hostname) -> Result { let pem = cert.to_pem()?; Response::builder() .status(StatusCode::OK) .header( http::header::ETAG, base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &*cert.digest(MessageDigest::sha256())?, ) .to_lowercase(), @@ -429,53 +428,156 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result, Error> .with_kind(ErrorKind::Network) } +fn parse_range(header: &HeaderValue, len: u64) -> Result<(u64, u64, u64), Error> { + let r = header + .to_str() + .with_kind(ErrorKind::Network)? + .trim() + .strip_prefix("bytes=") + .ok_or_else(|| Error::new(eyre!("invalid range units"), ErrorKind::InvalidRequest))?; + + if r.contains(",") { + return Err(Error::new( + eyre!("multi-range requests are unsupported"), + ErrorKind::InvalidRequest, + )); + } + if let Some((start, end)) = r.split_once("-").map(|(s, e)| (s.trim(), e.trim())) { + Ok(( + if start.is_empty() { + 0u64 + } else { + start.parse()? + }, + if end.is_empty() { + len - 1 + } else { + min(end.parse()?, len - 1) + }, + len, + )) + } else { + Ok((len - r.trim().parse::()?, len - 1, len)) + } +} + struct FileData { data: Body, len: Option, + content_range: Option<(u64, u64, u64)>, encoding: Option<&'static str>, - e_tag: String, - mime: Option, + e_tag: Option, + mime: Option, + digest: Option<(&'static str, Vec)>, } impl FileData { - fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self { + fn from_embedded( + req: &RequestParts, + file: &'static include_dir::File<'static>, + ) -> Result { let path = file.path(); - let (encoding, data) = req - .headers - .get_all(ACCEPT_ENCODING) - .into_iter() - .filter_map(|h| h.to_str().ok()) - .flat_map(|s| s.split(",")) - .filter_map(|s| s.split(";").next()) - .map(|s| s.trim()) - .fold((None, file.contents()), |acc, e| { - if let Some(file) = (e == "br") - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) - { - (Some("br"), file.contents()) - } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) - { - (Some("gzip"), file.contents()) - } else { - acc - } - }); + let (encoding, data, len, content_range) = if let Some(range) = req.headers.get(RANGE) { + let data = file.contents(); + let (start, end, size) = parse_range(range, data.len() as u64)?; + let encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + let data = if start > end { + &[] + } else { + &data[(start as usize)..=(end as usize)] + }; + let (len, data) = if encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(Cursor::new(data)))), + ) + } else { + (Some(data.len() as u64), Body::from(data)) + }; + (encoding, data, len, Some((start, end, size))) + } else { + let (encoding, data) = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .fold((None, file.contents()), |acc, e| { + if let Some(file) = (e == "br") + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) + { + (Some("br"), file.contents()) + } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) + { + (Some("gzip"), file.contents()) + } else { + acc + } + }); + (encoding, Body::from(data), Some(data.len() as u64), None) + }; - Self { - len: Some(data.len() as u64), + Ok(Self { + len, encoding, - data: data.into(), - e_tag: e_tag(path, None), + content_range, + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, + e_tag: file.metadata().map(|metadata| { + e_tag( + path, + format!( + "{}", + metadata + .modified() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1), + ) + .as_bytes(), + ) + }), mime: MimeGuess::from_path(path) .first() - .map(|m| m.essence_str().to_owned()), + .map(|m| m.essence_str().into()), + digest: None, + }) + } + + fn encode( + encoding: &mut Option<&str>, + data: R, + len: u64, + ) -> (Option, Body) { + if *encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(data)))), + ) + } else { + *encoding = None; + (Some(len), Body::from_stream(ReaderStream::new(data))) } } - async fn from_path(req: &RequestParts, path: &Path) -> Result { - let encoding = req + async fn from_path(req: &RequestParts, path: &Path) -> Result, Error> { + let mut encoding = req .headers .get_all(ACCEPT_ENCODING) .into_iter() @@ -486,75 +588,169 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = File::open(path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; + if tokio::fs::metadata(path).await.is_err() { + return Ok(None); + } + + let mut file = open_file(path).await?; + let metadata = file .metadata() .await .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; - let e_tag = e_tag(path, Some(&metadata)); + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, metadata.len())) + .transpose()?; - let (len, data) = if encoding == Some("gzip") { - ( - None, - Body::wrap_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))), + let e_tag = Some(e_tag( + path, + format!( + "{}", + metadata + .modified()? + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1) ) + .as_bytes(), + )); + + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + file.seek(std::io::SeekFrom::Start(start)).await?; + Self::encode(&mut encoding, file.take(len), len) } else { - ( - Some(metadata.len()), - Body::wrap_stream(ReaderStream::new(file)), - ) + Self::encode(&mut encoding, file, metadata.len()) }; - Ok(Self { - data, + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, len, + content_range, encoding, e_tag, mime: MimeGuess::from_path(path) .first() - .map(|m| m.essence_str().to_owned()), - }) + .map(|m| m.essence_str().into()), + digest: None, + })) } - async fn into_response(self, req: &RequestParts) -> Result, Error> { + async fn from_s9pk( + req: &RequestParts, + s9pk: &S9pk, + path: &Path, + ) -> Result, Error> { + let mut encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + + let Some(file) = s9pk.as_archive().contents().get_path(path) else { + return Ok(None); + }; + let Some(contents) = file.as_file() else { + return Ok(None); + }; + let (digest, len) = if let Some((hash, len)) = file.hash() { + (Some(("blake3", hash.as_bytes().to_vec())), len) + } else { + (None, contents.size().await?) + }; + + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, len)) + .transpose()?; + + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + Self::encode(&mut encoding, contents.slice(start, len).await?, len) + } else { + Self::encode(&mut encoding, contents.reader().await?.take(len), len) + }; + + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, + len, + content_range, + encoding, + e_tag: None, + mime: MimeGuess::from_path(path) + .first() + .map(|m| m.essence_str().into()), + digest, + })) + } + + fn into_response(self, req: &RequestParts) -> Result { let mut builder = Response::builder(); if let Some(mime) = self.mime { - builder = builder.header(http::header::CONTENT_TYPE, &*mime); + builder = builder.header(CONTENT_TYPE, &*mime); + } + if let Some(e_tag) = &self.e_tag { + builder = builder + .header(ETAG, &**e_tag) + .header(CACHE_CONTROL, "public, max-age=21000000, immutable"); + } + + builder = builder.header(ACCEPT_RANGES, "bytes"); + if let Some((start, end, size)) = self.content_range { + builder = builder + .header(CONTENT_RANGE, format!("bytes {start}-{end}/{size}")) + .status(StatusCode::PARTIAL_CONTENT); + } + + if let Some((algorithm, digest)) = self.digest { + builder = builder.header( + "Repr-Digest", + format!("{algorithm}=:{}:", Base64Display::new(&digest, &BASE64)), + ); } - builder = builder.header(http::header::ETAG, &*self.e_tag); - builder = builder.header( - http::header::CACHE_CONTROL, - "public, max-age=21000000, immutable", - ); if req .headers - .get_all(http::header::CONNECTION) + .get_all(CONNECTION) .iter() .flat_map(|s| s.to_str().ok()) .flat_map(|s| s.split(",")) .any(|s| s.trim() == "keep-alive") { - builder = builder.header(http::header::CONNECTION, "keep-alive"); + builder = builder.header(CONNECTION, "keep-alive"); } - if req - .headers - .get("if-none-match") - .and_then(|h| h.to_str().ok()) - == Some(self.e_tag.as_ref()) + if self.e_tag.is_some() + && req + .headers + .get("if-none-match") + .and_then(|h| h.to_str().ok()) + == self.e_tag.as_deref() { - builder = builder.status(StatusCode::NOT_MODIFIED); - builder.body(Body::empty()) + builder.status(StatusCode::NOT_MODIFIED).body(Body::empty()) } else { if let Some(len) = self.len { - builder = builder.header(http::header::CONTENT_LENGTH, len); + builder = builder.header(CONTENT_LENGTH, len); } if let Some(encoding) = self.encoding { - builder = builder.header(http::header::CONTENT_ENCODING, encoding); + builder = builder.header(CONTENT_ENCODING, encoding); } builder.body(self.data) @@ -563,24 +759,17 @@ impl FileData { } } -fn e_tag(path: &Path, metadata: Option<&Metadata>) -> String { +lazy_static::lazy_static! { + static ref INSTANCE_NONCE: u64 = rand::random(); +} + +fn e_tag(path: &Path, modified: impl AsRef<[u8]>) -> String { let mut hasher = sha2::Sha256::new(); hasher.update(format!("{:?}", path).as_bytes()); - if let Some(modified) = metadata.and_then(|m| m.modified().ok()) { - hasher.update( - format!( - "{}", - modified - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - ) - .as_bytes(), - ); - } + hasher.update(modified.as_ref()); let res = hasher.finalize(); format!( "\"{}\"", - base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase() + base32::encode(base32::Alphabet::Rfc4648 { padding: false }, res.as_slice()).to_lowercase() ) } diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 1bf4c5f44..bba50c371 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -4,7 +4,7 @@ use std::sync::atomic::AtomicBool; use std::sync::{Arc, Weak}; use std::time::Duration; -use clap::ArgMatches; +use clap::Parser; use color_eyre::eyre::eyre; use futures::future::BoxFuture; use futures::{FutureExt, TryStreamExt}; @@ -12,8 +12,8 @@ use helpers::NonDetachingJoinHandle; use itertools::Itertools; use lazy_static::lazy_static; use regex::Regex; -use rpc_toolkit::command; -use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; use tokio::process::Command; use tokio::sync::{mpsc, oneshot}; @@ -21,19 +21,50 @@ use tokio::time::Instant; use torut::control::{AsyncEvent, AuthenticatedConn, ConnError}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; +use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::logs::{ - cli_logs_generic_follow, cli_logs_generic_nofollow, fetch_logs, follow_logs, journalctl, - LogFollowResponse, LogResponse, LogSource, -}; -use crate::util::serde::{display_serializable, IoFormat}; -use crate::util::{display_none, Invoke}; -use crate::{Error, ErrorKind, ResultExt as _}; +use crate::logs::{journalctl, LogSource, LogsParams}; +use crate::prelude::*; +use crate::util::serde::{display_serializable, Base64, HandlerExtSerde, WithIoFormat}; +use crate::util::Invoke; pub const SYSTEMD_UNIT: &str = "tor@default"; const STARTING_HEALTH_TIMEOUT: u64 = 120; // 2min +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct OnionStore(BTreeMap); +impl Map for OnionStore { + type Key = OnionAddressV3; + type Value = TorSecretKeyV3; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key.get_address_without_dot_onion()) + } +} +impl OnionStore { + pub fn new() -> Self { + Self::default() + } + pub fn insert(&mut self, key: TorSecretKeyV3) { + self.0.insert(key.public().get_onion_address(), key); + } +} +impl Model { + pub fn new_key(&mut self) -> Result { + let key = TorSecretKeyV3::generate(); + self.insert(&key.public().get_onion_address(), &key)?; + Ok(key) + } + pub fn insert_key(&mut self, key: &TorSecretKeyV3) -> Result<(), Error> { + self.insert(&key.public().get_onion_address(), &key) + } + pub fn get_key(&self, address: &OnionAddressV3) -> Result { + self.as_idx(address) + .or_not_found(lazy_format!("private key for {address}"))? + .de() + } +} + enum ErrorLogSeverity { Fatal { wipe_state: bool }, Unknown { wipe_state: bool }, @@ -53,16 +84,123 @@ lazy_static! { static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap(); } -#[command(subcommands(list_services, logs, reset))] -pub fn tor() -> Result<(), Error> { - Ok(()) +pub fn tor() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list-services", + from_fn_async(list_services) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_services(handle.params, result)) + }) + .with_about("Display Tor V3 Onion Addresses") + .with_call_remote::(), + ) + .subcommand("logs", logs().with_about("Display Tor logs")) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::) + .no_display() + .with_about("Display Tor logs"), + ) + .subcommand( + "reset", + from_fn_async(reset) + .no_display() + .with_about("Reset Tor daemon") + .with_call_remote::(), + ) + .subcommand( + "key", + key::().with_about("Manage the onion service key store"), + ) +} + +pub fn key() -> ParentHandler { + ParentHandler::new() + .subcommand( + "generate", + from_fn_async(generate_key) + .with_about("Generate an onion service key and add it to the key store") + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add_key) + .with_about("Add an onion service key to the key store") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_keys) + .with_custom_display_fn(|_, res| { + for addr in res { + println!("{addr}"); + } + Ok(()) + }) + .with_about("List onion services with keys in the key store") + .with_call_remote::(), + ) +} + +pub async fn generate_key(ctx: RpcContext) -> Result { + ctx.db + .mutate(|db| { + Ok(db + .as_private_mut() + .as_key_store_mut() + .as_onion_mut() + .new_key()? + .public() + .get_onion_address()) + }) + .await +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddKeyParams { + pub key: Base64<[u8; 64]>, +} + +pub async fn add_key( + ctx: RpcContext, + AddKeyParams { key }: AddKeyParams, +) -> Result { + let key = TorSecretKeyV3::from(key.0); + ctx.db + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_onion_mut() + .insert_key(&key) + }) + .await?; + Ok(key.public().get_onion_address()) +} + +pub async fn list_keys(ctx: RpcContext) -> Result, Error> { + ctx.db + .peek() + .await + .into_private() + .into_key_store() + .into_onion() + .keys() +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ResetParams { + #[arg(name = "wipe-state", short = 'w', long = "wipe-state")] + wipe_state: bool, + reason: String, } -#[command(display(display_none))] pub async fn reset( - #[context] ctx: RpcContext, - #[arg(rename = "wipe-state", short = 'w', long = "wipe-state")] wipe_state: bool, - #[arg] reason: String, + ctx: RpcContext, + ResetParams { reason, wipe_state }: ResetParams, ) -> Result<(), Error> { ctx.net_controller .tor @@ -70,11 +208,11 @@ pub async fn reset( .await } -fn display_services(services: Vec, matches: &ArgMatches) { +pub fn display_services(params: WithIoFormat, services: Vec) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(services, matches); + if let Some(format) = params.format { + return display_serializable(format, services); } let mut table = Table::new(); @@ -85,64 +223,14 @@ fn display_services(services: Vec, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(rename = "list-services", display(display_services))] -pub async fn list_services( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result, Error> { +pub async fn list_services(ctx: RpcContext, _: Empty) -> Result, Error> { ctx.net_controller.tor.list_services().await } -#[command( - custom_cli(cli_logs(async, context(CliContext))), - subcommands(self(logs_nofollow(async)), logs_follow), - display(display_none) -)] -pub async fn logs( - #[arg(short = 'l', long = "limit")] limit: Option, - #[arg(short = 'c', long = "cursor")] cursor: Option, - #[arg(short = 'B', long = "before", default)] before: bool, - #[arg(short = 'f', long = "follow", default)] follow: bool, -) -> Result<(Option, Option, bool, bool), Error> { - Ok((limit, cursor, before, follow)) -} -pub async fn cli_logs( - ctx: CliContext, - (limit, cursor, before, follow): (Option, Option, bool, bool), -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "net.tor.logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "net.tor.logs", None, limit, cursor, before).await - } -} -pub async fn logs_nofollow( - _ctx: (), - (limit, cursor, before, _): (Option, Option, bool, bool), -) -> Result { - fetch_logs(LogSource::Unit(SYSTEMD_UNIT), limit, cursor, before).await -} - -#[command(rpc_only, rename = "follow", display(display_none))] -pub async fn logs_follow( - #[context] ctx: RpcContext, - #[parent_data] (limit, _, _, _): (Option, Option, bool, bool), -) -> Result { - follow_logs(ctx, LogSource::Unit(SYSTEMD_UNIT), limit).await +pub fn logs() -> ParentHandler { + crate::logs::logs::(|_: &RpcContext, _| async { + Ok(LogSource::Unit(SYSTEMD_UNIT)) + }) } fn event_handler(_event: AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>> { @@ -158,33 +246,29 @@ impl TorController { pub async fn add( &self, key: TorSecretKeyV3, - external: u16, - target: SocketAddr, - ) -> Result, Error> { + bindings: Vec<(u16, SocketAddr)>, + ) -> Result>, Error> { let (reply, res) = oneshot::channel(); self.0 .send .send(TorCommand::AddOnion { key, - external, - target, + bindings, reply, }) - .ok() - .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor))?; + .map_err(|_| Error::new(eyre!("TorControl died"), ErrorKind::Tor))?; res.await - .ok() - .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor)) + .map_err(|_| Error::new(eyre!("TorControl died"), ErrorKind::Tor)) } pub async fn gc( &self, - key: Option, + addr: Option, external: Option, ) -> Result<(), Error> { self.0 .send - .send(TorCommand::GC { key, external }) + .send(TorCommand::GC { addr, external }) .ok() .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor)) } @@ -216,7 +300,7 @@ impl TorController { .lines() .map(|l| l.trim()) .filter(|l| !l.is_empty()) - .map(|l| l.parse().with_kind(ErrorKind::Tor)) + .map(|l| l.parse::().with_kind(ErrorKind::Tor)) .collect() } } @@ -229,12 +313,11 @@ type AuthenticatedConnection = AuthenticatedConn< enum TorCommand { AddOnion { key: TorSecretKeyV3, - external: u16, - target: SocketAddr, - reply: oneshot::Sender>, + bindings: Vec<(u16, SocketAddr)>, + reply: oneshot::Sender>>, }, GC { - key: Option, + addr: Option, external: Option, }, GetInfo { @@ -252,7 +335,13 @@ async fn torctl( tor_control: SocketAddr, tor_socks: SocketAddr, recv: &mut mpsc::UnboundedReceiver, - services: &mut BTreeMap<[u8; 64], BTreeMap>>>, + services: &mut BTreeMap< + OnionAddressV3, + ( + TorSecretKeyV3, + BTreeMap>>, + ), + >, wipe_state: &AtomicBool, health_timeout: &mut Duration, ) -> Result<(), Error> { @@ -300,7 +389,15 @@ async fn torctl( .invoke(ErrorKind::Tor) .await?; - let logs = journalctl(LogSource::Unit(SYSTEMD_UNIT), 0, None, false, true).await?; + let logs = journalctl( + LogSource::Unit(SYSTEMD_UNIT), + Some(0), + None, + Some("0"), + false, + true, + ) + .await?; let mut tcp_stream = None; for _ in 0..60 { @@ -370,27 +467,32 @@ async fn torctl( match command { TorCommand::AddOnion { key, - external, - target, + bindings, reply, } => { - let mut service = if let Some(service) = services.remove(&key.as_bytes()) { + let addr = key.public().get_onion_address(); + let mut service = if let Some((_key, service)) = services.remove(&addr) { + debug_assert_eq!(key, _key); service } else { BTreeMap::new() }; - let mut binding = service.remove(&external).unwrap_or_default(); - let rc = if let Some(rc) = - Weak::upgrade(&binding.remove(&target).unwrap_or_default()) - { - rc - } else { - Arc::new(()) - }; - binding.insert(target, Arc::downgrade(&rc)); - service.insert(external, binding); - services.insert(key.as_bytes(), service); - reply.send(rc).unwrap_or_default(); + let mut rcs = Vec::with_capacity(bindings.len()); + for (external, target) in bindings { + let mut binding = service.remove(&external).unwrap_or_default(); + let rc = if let Some(rc) = + Weak::upgrade(&binding.remove(&target).unwrap_or_default()) + { + rc + } else { + Arc::new(()) + }; + binding.insert(target, Arc::downgrade(&rc)); + service.insert(external, binding); + rcs.push(rc); + } + services.insert(addr, (key, service)); + reply.send(rcs).unwrap_or_default(); } TorCommand::GetInfo { reply, .. } => { reply @@ -430,8 +532,7 @@ async fn torctl( ) .await?; - for (key, service) in std::mem::take(services) { - let key = TorSecretKeyV3::from(key); + for (addr, (key, service)) in std::mem::take(services) { let bindings = service .iter() .flat_map(|(ext, int)| { @@ -441,7 +542,7 @@ async fn torctl( }) .collect::>(); if !bindings.is_empty() { - services.insert(key.as_bytes(), service); + services.insert(addr, (key.clone(), service)); connection .add_onion_v3(&key, false, false, false, None, &mut bindings.iter()) .await?; @@ -453,31 +554,33 @@ async fn torctl( match command { TorCommand::AddOnion { key, - external, - target, + bindings, reply, } => { let mut rm_res = Ok(()); - let onion_base = key - .public() - .get_onion_address() - .get_address_without_dot_onion(); - let mut service = if let Some(service) = services.remove(&key.as_bytes()) { + let addr = key.public().get_onion_address(); + let onion_base = addr.get_address_without_dot_onion(); + let mut service = if let Some((_key, service)) = services.remove(&addr) { + debug_assert_eq!(_key, key); rm_res = connection.del_onion(&onion_base).await; service } else { BTreeMap::new() }; - let mut binding = service.remove(&external).unwrap_or_default(); - let rc = if let Some(rc) = - Weak::upgrade(&binding.remove(&target).unwrap_or_default()) - { - rc - } else { - Arc::new(()) - }; - binding.insert(target, Arc::downgrade(&rc)); - service.insert(external, binding); + let mut rcs = Vec::with_capacity(bindings.len()); + for (external, target) in bindings { + let mut binding = service.remove(&external).unwrap_or_default(); + let rc = if let Some(rc) = + Weak::upgrade(&binding.remove(&target).unwrap_or_default()) + { + rc + } else { + Arc::new(()) + }; + binding.insert(target, Arc::downgrade(&rc)); + service.insert(external, binding); + rcs.push(rc); + } let bindings = service .iter() .flat_map(|(ext, int)| { @@ -486,25 +589,21 @@ async fn torctl( .map(|(addr, _)| (*ext, SocketAddr::from(*addr))) }) .collect::>(); - services.insert(key.as_bytes(), service); - reply.send(rc).unwrap_or_default(); + services.insert(addr, (key.clone(), service)); + reply.send(rcs).unwrap_or_default(); rm_res?; connection .add_onion_v3(&key, false, false, false, None, &mut bindings.iter()) .await?; } - TorCommand::GC { key, external } => { - for key in if key.is_some() { - itertools::Either::Left(key.into_iter().map(|k| k.as_bytes())) + TorCommand::GC { addr, external } => { + for addr in if addr.is_some() { + itertools::Either::Left(addr.into_iter()) } else { itertools::Either::Right(services.keys().cloned().collect_vec().into_iter()) } { - let key = TorSecretKeyV3::from(key); - let onion_base = key - .public() - .get_onion_address() - .get_address_without_dot_onion(); - if let Some(mut service) = services.remove(&key.as_bytes()) { + if let Some((key, mut service)) = services.remove(&addr) { + let onion_base: String = addr.get_address_without_dot_onion(); for external in if external.is_some() { itertools::Either::Left(external.into_iter()) } else { @@ -533,7 +632,7 @@ async fn torctl( }) .collect::>(); if !bindings.is_empty() { - services.insert(key.as_bytes(), service); + services.insert(addr, (key.clone(), service)); } rm_res?; if !bindings.is_empty() { @@ -684,7 +783,7 @@ async fn test() { let mut conn = torut::control::UnauthenticatedConn::new( TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], 9051))) .await - .unwrap(), // TODO + .unwrap(), ); let auth = conn .load_protocol_info() diff --git a/core/startos/src/net/utils.rs b/core/startos/src/net/utils.rs index e496bd1f7..8799fd6cd 100644 --- a/core/startos/src/net/utils.rs +++ b/core/startos/src/net/utils.rs @@ -1,17 +1,32 @@ -use std::convert::Infallible; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::collections::BTreeMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6}; use std::path::Path; use async_stream::try_stream; use color_eyre::eyre::eyre; use futures::stream::BoxStream; use futures::{StreamExt, TryStreamExt}; -use ipnet::{Ipv4Net, Ipv6Net}; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +use nix::net::if_::if_nametoindex; use tokio::net::{TcpListener, TcpStream}; use tokio::process::Command; +use crate::db::model::public::NetworkInterfaceInfo; +use crate::net::network_interface::NetworkInterfaceListener; +use crate::net::web_server::Accept; +use crate::prelude::*; +use crate::util::sync::Watch; use crate::util::Invoke; -use crate::Error; + +pub fn ipv6_is_link_local(addr: Ipv6Addr) -> bool { + (addr.segments()[0] & 0xffc0) == 0xfe80 +} + +pub fn ipv6_is_local(addr: Ipv6Addr) -> bool { + addr.is_loopback() || (addr.segments()[0] & 0xfe00) == 0xfc00 || ipv6_is_link_local(addr) +} fn parse_iface_ip(output: &str) -> Result, Error> { let output = output.trim(); @@ -113,22 +128,53 @@ pub async fn find_eth_iface() -> Result { )) } -#[pin_project::pin_project] -pub struct SingleAccept(Option); -impl SingleAccept { - pub fn new(conn: T) -> Self { - Self(Some(conn)) - } -} -impl hyper::server::accept::Accept for SingleAccept { - type Conn = T; - type Error = Infallible; - fn poll_accept( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll>> { - std::task::Poll::Ready(self.project().0.take().map(Ok)) +pub async fn all_socket_addrs_for(port: u16) -> Result, Error> { + let mut res = Vec::new(); + + let raw = String::from_utf8( + Command::new("ip") + .arg("-o") + .arg("addr") + .arg("show") + .invoke(ErrorKind::ParseSysInfo) + .await?, + )?; + let err = |item: &str, lineno: usize, line: &str| { + Error::new( + eyre!("failed to parse ip info ({item}[line:{lineno}]) from {line:?}"), + ErrorKind::ParseSysInfo, + ) + }; + for (idx, line) in raw + .lines() + .map(|l| l.trim()) + .enumerate() + .filter(|(_, l)| !l.is_empty()) + { + let mut split = line.split_whitespace(); + let _num = split.next(); + let ifname = split.next().ok_or_else(|| err("ifname", idx, line))?; + let _kind = split.next(); + let ipnet_str = split.next().ok_or_else(|| err("ipnet", idx, line))?; + let ipnet = ipnet_str + .parse::() + .with_ctx(|_| (ErrorKind::ParseSysInfo, err("ipnet", idx, ipnet_str)))?; + match ipnet.addr() { + IpAddr::V4(ip4) => res.push((ifname.into(), SocketAddr::new(ip4.into(), port))), + IpAddr::V6(ip6) => res.push(( + ifname.into(), + SocketAddr::V6(SocketAddrV6::new( + ip6, + port, + 0, + if_nametoindex(ifname) + .with_ctx(|_| (ErrorKind::ParseSysInfo, "reading scope_id"))?, + )), + )), + } } + + Ok(res) } pub struct TcpListeners { @@ -147,20 +193,21 @@ impl TcpListeners { .0 } } -impl hyper::server::accept::Accept for TcpListeners { - type Conn = TcpStream; - type Error = std::io::Error; - - fn poll_accept( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll>> { - for listener in self.listeners.iter() { - let poll = listener.poll_accept(cx); - if poll.is_ready() { - return poll.map(|a| a.map(|a| a.0)).map(Some); - } - } - std::task::Poll::Pending - } -} +// impl hyper::server::accept::Accept for TcpListeners { +// type Conn = TcpStream; +// type Error = std::io::Error; + +// fn poll_accept( +// self: std::pin::Pin<&mut Self>, +// cx: &mut std::task::Context<'_>, +// ) -> std::task::Poll>> { +// for listener in self.listeners.iter() { +// let poll = listener.poll_accept(cx); +// if poll.is_ready() { +// return poll.map(|a| a.map(|a| a.0)).map(Some); +// } +// } +// std::task::Poll::Pending +// } +// } +// TODO diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index bfbba0572..cbc1cc916 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -1,378 +1,717 @@ -use std::collections::BTreeMap; -use std::convert::Infallible; -use std::net::{IpAddr, Ipv6Addr, SocketAddr}; -use std::str::FromStr; +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{IpAddr, SocketAddr}; use std::sync::{Arc, Weak}; use std::time::Duration; +use async_acme::acme::{Identifier, ACME_TLS_ALPN_NAME}; +use axum::body::Body; +use axum::extract::Request; +use axum::response::Response; use color_eyre::eyre::eyre; +use futures::FutureExt; use helpers::NonDetachingJoinHandle; -use http::{Response, Uri}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Body; +use http::Uri; +use imbl_value::InternedString; use models::ResultExt; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::{Mutex, RwLock}; -use tokio_rustls::rustls::server::Acceptor; +use rpc_toolkit::{from_fn, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; +use tokio::sync::watch; +use tokio_rustls::rustls::crypto::CryptoProvider; +use tokio_rustls::rustls::pki_types::{ + CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, +}; +use tokio_rustls::rustls::server::{Acceptor, ResolvesServerCert}; +use tokio_rustls::rustls::sign::CertifiedKey; use tokio_rustls::rustls::{RootCertStore, ServerConfig}; use tokio_rustls::{LazyConfigAcceptor, TlsConnector}; +use tokio_stream::wrappers::WatchStream; +use tokio_stream::StreamExt; use tracing::instrument; +use ts_rs::TS; -use crate::net::keys::Key; -use crate::net::ssl::SslManager; -use crate::net::utils::SingleAccept; +use crate::context::{CliContext, RpcContext}; +use crate::db::model::Database; +use crate::net::acme::{AcmeCertCache, AcmeProvider}; +use crate::net::network_interface::{ + Accepted, NetworkInterfaceController, NetworkInterfaceListener, +}; +use crate::net::static_server::server_error; use crate::prelude::*; -use crate::util::io::{BackTrackingReader, TimeoutStream}; +use crate::util::io::BackTrackingIO; +use crate::util::serde::{display_serializable, HandlerExtSerde, MaybeUtf8String}; +use crate::util::sync::SyncMutex; + +pub fn vhost_api() -> ParentHandler { + ParentHandler::new().subcommand( + "dump-table", + from_fn(|ctx: RpcContext| Ok(ctx.net_controller.vhost.dump_table())) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; + + if let Some(format) = params.format { + display_serializable(format, res); + return Ok::<_, Error>(()); + } + + let mut table = Table::new(); + table.add_row(row![bc => "FROM", "TO", "PUBLIC", "ACME", "CONNECT SSL", "ACTIVE"]); + + for (external, targets) in res { + for (host, targets) in targets { + for (idx, target) in targets.into_iter().enumerate() { + table.add_row(row![ + format!( + "{}:{}", + host.as_ref().map(|s| &**s).unwrap_or("*"), + external.0 + ), + target.addr, + target.public, + target.acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE"), + target.connect_ssl.is_ok(), + idx == 0 + ]); + } + } + } + + table.print_tty(false)?; + + Ok(()) + }) + .with_call_remote::(), + ) +} + +#[derive(Debug)] +struct SingleCertResolver(Arc); +impl ResolvesServerCert for SingleCertResolver { + fn resolve(&self, _: tokio_rustls::rustls::server::ClientHello) -> Option> { + Some(self.0.clone()) + } +} // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 pub struct VHostController { - ssl: Arc, - servers: Mutex>, + db: TypedPatchDb, + interfaces: Arc, + crypto_provider: Arc, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + servers: SyncMutex>, } impl VHostController { - pub fn new(ssl: Arc) -> Self { + pub fn new(db: TypedPatchDb, interfaces: Arc) -> Self { Self { - ssl, - servers: Mutex::new(BTreeMap::new()), + db, + interfaces, + crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), + acme_tls_alpn_cache: Arc::new(SyncMutex::new(BTreeMap::new())), + servers: SyncMutex::new(BTreeMap::new()), } } #[instrument(skip_all)] - pub async fn add( + pub fn add( &self, - key: Key, - hostname: Option, + hostname: Option, external: u16, - target: SocketAddr, - connect_ssl: Result<(), AlpnInfo>, + TargetInfo { + public, + acme, + addr, + connect_ssl, + }: TargetInfo, ) -> Result, Error> { - let mut writable = self.servers.lock().await; - let server = if let Some(server) = writable.remove(&external) { - server - } else { - VHostServer::new(external, self.ssl.clone()).await? - }; - let rc = server - .add( + self.servers.mutate(|writable| { + let server = if let Some(server) = writable.remove(&external) { + server + } else { + VHostServer::new( + external, + self.db.clone(), + self.interfaces.clone(), + self.crypto_provider.clone(), + self.acme_tls_alpn_cache.clone(), + )? + }; + let rc = server.add( hostname, TargetInfo { - addr: target, + public, + acme, + addr, connect_ssl, - key, }, - ) - .await; - writable.insert(external, server); - Ok(rc?) + ); + writable.insert(external, server); + Ok(rc?) + }) } + + pub fn dump_table( + &self, + ) -> BTreeMap, BTreeMap>, BTreeSet>> + { + self.servers.peek(|s| { + s.iter() + .map(|(k, v)| { + ( + JsonKey::new(*k), + v.mapping + .borrow() + .iter() + .map(|(k, v)| { + ( + JsonKey::new(k.clone()), + v.iter() + .filter(|(_, v)| v.strong_count() > 0) + .map(|(k, _)| k) + .cloned() + .collect(), + ) + }) + .collect(), + ) + }) + .collect() + }) + } + #[instrument(skip_all)] - pub async fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { - let mut writable = self.servers.lock().await; - if let Some(server) = writable.remove(&external) { - server.gc(hostname).await?; - if !server.is_empty().await? { - writable.insert(external, server); + pub fn gc(&self, hostname: Option, external: u16) { + self.servers.mutate(|writable| { + if let Some(server) = writable.remove(&external) { + server.gc(hostname); + if !server.is_empty() { + writable.insert(external, server); + } } - } - Ok(()) + }) } } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -struct TargetInfo { - addr: SocketAddr, - connect_ssl: Result<(), AlpnInfo>, - key: Key, +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct TargetInfo { + pub public: bool, + pub acme: Option, + pub addr: SocketAddr, + pub connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] pub enum AlpnInfo { Reflect, - Specified(Vec>), + Specified(Vec), +} +impl Default for AlpnInfo { + fn default() -> Self { + Self::Reflect + } } +type AcmeTlsAlpnCache = + Arc>>>>>; +type Mapping = BTreeMap, BTreeMap>>; + struct VHostServer { - mapping: Weak, BTreeMap>>>>, + mapping: watch::Sender, _thread: NonDetachingJoinHandle<()>, } + impl VHostServer { - #[instrument(skip_all)] - async fn new(port: u16, ssl: Arc) -> Result { - // check if port allowed - let listener = TcpListener::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port)) - .await - .with_kind(crate::ErrorKind::Network)?; - let mapping = Arc::new(RwLock::new(BTreeMap::new())); - Ok(Self { - mapping: Arc::downgrade(&mapping), - _thread: tokio::spawn(async move { - loop { - match listener.accept().await { - Ok((stream, _)) => { - let stream = - Box::pin(TimeoutStream::new(stream, Duration::from_secs(300))); - let mut stream = BackTrackingReader::new(stream); - stream.start_buffering(); - let mapping = mapping.clone(); - let ssl = ssl.clone(); - tokio::spawn(async move { - if let Err(e) = async { - let mid = match LazyConfigAcceptor::new( - Acceptor::default(), - &mut stream, - ) - .await - { - Ok(a) => a, - Err(_) => { - stream.rewind(); - return hyper::server::Server::builder( - SingleAccept::new(stream), - ) - .serve(make_service_fn(|_| async { - Ok::<_, Infallible>(service_fn(|req| async move { - let host = req - .headers() - .get(http::header::HOST) - .and_then(|host| host.to_str().ok()); - let uri = Uri::from_parts({ - let mut parts = - req.uri().to_owned().into_parts(); - parts.authority = host - .map(FromStr::from_str) - .transpose()?; - parts - })?; - Response::builder() - .status( - http::StatusCode::TEMPORARY_REDIRECT, - ) - .header( - http::header::LOCATION, - uri.to_string(), - ) - .body(Body::default()) - })) - })) - .await - .with_kind(crate::ErrorKind::Network); - } - }; - let target_name = - mid.client_hello().server_name().map(|s| s.to_owned()); - let target = { - let mapping = mapping.read().await; - mapping - .get(&target_name) - .into_iter() - .flatten() - .find(|(_, rc)| rc.strong_count() > 0) - .or_else(|| { - if target_name - .map(|s| s.parse::().is_ok()) - .unwrap_or(true) - { - mapping - .get(&None) - .into_iter() - .flatten() - .find(|(_, rc)| rc.strong_count() > 0) - } else { - None - } - }) - .map(|(target, _)| target.clone()) - }; - if let Some(target) = target { - let mut tcp_stream = - TcpStream::connect(target.addr).await?; - let key = - ssl.with_certs(target.key, target.addr.ip()).await?; - let cfg = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth(); - let mut cfg = - if mid.client_hello().signature_schemes().contains( - &tokio_rustls::rustls::SignatureScheme::ED25519, - ) { - cfg.with_single_cert( - key.fullchain_ed25519() - .into_iter() - .map(|c| { - Ok(tokio_rustls::rustls::Certificate( - c.to_der()?, - )) - }) - .collect::>()?, - tokio_rustls::rustls::PrivateKey( - key.key() - .openssl_key_ed25519() - .private_key_to_der()?, - ), - ) + async fn accept( + listener: &mut NetworkInterfaceListener, + mut mapping: watch::Receiver, + db: TypedPatchDb, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + crypto_provider: Arc, + ) -> Result<(), Error> { + let accepted; + + loop { + let any_public = mapping + .borrow() + .iter() + .any(|(_, targets)| targets.iter().any(|(target, _)| target.public)); + + let changed_public = mapping + .wait_for(|m| { + m.iter() + .any(|(_, targets)| targets.iter().any(|(target, _)| target.public)) + != any_public + }) + .boxed(); + + tokio::select! { + a = listener.accept(any_public) => { + accepted = a?; + break; + } + _ = changed_public => { + tracing::debug!("port {} {} public bindings", listener.port(), if any_public { "no longer has" } else { "now has" }); + } + } + } + + if let Err(e) = socket2::SockRef::from(&accepted.stream).set_tcp_keepalive( + &socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(900)) + .with_interval(Duration::from_secs(60)) + .with_retries(5), + ) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } + + tokio::spawn(async move { + let bind = accepted.bind; + if let Err(e) = + Self::handle_stream(accepted, mapping, db, acme_tls_alpn_cache, crypto_provider) + .await + { + tracing::error!("Error in VHostController on {bind}: {e}"); + tracing::debug!("{e:?}") + } + }); + Ok(()) + } + + async fn handle_stream( + Accepted { + stream, + is_public, + wan_ip, + bind, + .. + }: Accepted, + mapping: watch::Receiver, + db: TypedPatchDb, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + crypto_provider: Arc, + ) -> Result<(), Error> { + let mut stream = BackTrackingIO::new(stream); + let mid: tokio_rustls::StartHandshake<&mut BackTrackingIO> = + match LazyConfigAcceptor::new(Acceptor::default(), &mut stream).await { + Ok(a) => a, + Err(e) => { + let (_, buf) = stream.rewind(); + if std::str::from_utf8(buf) + .ok() + .and_then(|buf| { + buf.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .next() + }) + .map_or(false, |buf| { + regex::Regex::new("[A-Z]+ (.+) HTTP/1") + .unwrap() + .is_match(buf) + }) + { + return hyper_util::server::conn::auto::Builder::new( + hyper_util::rt::TokioExecutor::new(), + ) + .serve_connection( + hyper_util::rt::TokioIo::new(stream), + hyper_util::service::TowerToHyperService::new( + axum::Router::new().fallback(axum::routing::method_routing::any( + move |req: Request| async move { + match async move { + let host = req + .headers() + .get(http::header::HOST) + .and_then(|host| host.to_str().ok()); + if let Some(host) = host { + let uri = Uri::from_parts({ + let mut parts = + req.uri().to_owned().into_parts(); + parts.scheme = Some("https".parse()?); + parts.authority = Some(host.parse()?); + parts + })?; + Response::builder() + .status(http::StatusCode::TEMPORARY_REDIRECT) + .header(http::header::LOCATION, uri.to_string()) + .body(Body::default()) } else { - cfg.with_single_cert( - key.fullchain_nistp256() - .into_iter() - .map(|c| { - Ok(tokio_rustls::rustls::Certificate( - c.to_der()?, - )) - }) - .collect::>()?, - tokio_rustls::rustls::PrivateKey( - key.key() - .openssl_key_nistp256() - .private_key_to_der()?, - ), - ) - } - .with_kind(crate::ErrorKind::OpenSsl)?; - match target.connect_ssl { - Ok(()) => { - let mut client_cfg = - tokio_rustls::rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates({ - let mut store = RootCertStore::empty(); - store.add( - &tokio_rustls::rustls::Certificate( - key.root_ca().to_der()?, - ), - ).with_kind(crate::ErrorKind::OpenSsl)?; - store - }) - .with_no_client_auth(); - client_cfg.alpn_protocols = mid - .client_hello() - .alpn() - .into_iter() - .flatten() - .map(|x| x.to_vec()) - .collect(); - let mut target_stream = - TlsConnector::from(Arc::new(client_cfg)) - .connect_with( - key.key() - .internal_address() - .as_str() - .try_into() - .with_kind( - crate::ErrorKind::OpenSsl, - )?, - tcp_stream, - |conn| { - cfg.alpn_protocols.extend( - conn.alpn_protocol() - .into_iter() - .map(|p| p.to_vec()), - ) - }, - ) - .await - .with_kind(crate::ErrorKind::OpenSsl)?; - let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tls_stream.get_mut().0.stop_buffering(); - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut target_stream, - ) - .await - } - Err(AlpnInfo::Reflect) => { - for proto in - mid.client_hello().alpn().into_iter().flatten() - { - cfg.alpn_protocols.push(proto.into()); - } - let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tls_stream.get_mut().0.stop_buffering(); - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut tcp_stream, - ) - .await + Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .body(Body::from("Host header required")) } - Err(AlpnInfo::Specified(alpn)) => { - cfg.alpn_protocols = alpn; - let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { - Ok(a) => a, - Err(e) => { - tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); - tracing::trace!("{e:?}"); - return Ok(()) - } - }; - tls_stream.get_mut().0.stop_buffering(); - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut tcp_stream, - ) - .await + } + .await + { + Ok(a) => a, + Err(e) => { + tracing::warn!( + "Error redirecting http request on ssl port: {e}" + ); + tracing::error!("{e:?}"); + server_error(Error::new(e, ErrorKind::Network)) } } - .map_or_else( - |e| { - use std::io::ErrorKind as E; - match e.kind() { - E::UnexpectedEof | E::BrokenPipe | E::ConnectionAborted | E::ConnectionReset | E::ConnectionRefused | E::TimedOut | E::Interrupted | E::NotConnected => Ok(()), - _ => Err(e), - }}, - |_| Ok(()), - )?; - } else { - // 503 - } - Ok::<_, Error>(()) - } - .await - { - tracing::error!("Error in VHostController on port {port}: {e}"); - tracing::debug!("{e:?}") - } - }); + }, + )), + ), + ) + .await + .map_err(|e| { + Error::new(color_eyre::eyre::Report::msg(e), ErrorKind::Network) + }); + } else { + return Err(e).with_kind(ErrorKind::Network); + } + } + }; + let target_name: Option = + mid.client_hello().server_name().map(|s| s.into()); + if let Some(domain) = target_name.as_ref() { + if mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .any(|alpn| alpn == ACME_TLS_ALPN_NAME) + { + let cert = WatchStream::new( + acme_tls_alpn_cache + .peek(|c| c.get(&**domain).cloned()) + .ok_or_else(|| { + Error::new( + eyre!("No challenge recv available for {domain}"), + ErrorKind::OpenSsl, + ) + })?, + ); + tracing::info!("Waiting for verification cert for {domain}"); + let cert = cert + .filter(|c| c.is_some()) + .next() + .await + .flatten() + .ok_or_else(|| { + Error::new( + eyre!("No challenge available for {domain}"), + ErrorKind::OpenSsl, + ) + })?; + tracing::info!("Verification cert received for {domain}"); + let mut cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(cert))); + + cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()]; + tracing::info!("performing ACME auth challenge"); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + accept.await?; + tracing::info!("ACME auth challenge completed"); + return Ok(()); + } + } + let target = { + let m = mapping.borrow(); + m.get(&target_name) + .into_iter() + .flatten() + .find(|(_, rc)| rc.strong_count() > 0) + .or_else(|| { + if target_name + .as_ref() + .map(|s| s.parse::().is_ok()) + .unwrap_or(true) + { + m.get(&None) + .into_iter() + .flatten() + .find(|(_, rc)| rc.strong_count() > 0) + } else { + None + } + }) + .map(|(target, _)| target.clone()) + }; + if let Some(target) = target { + if is_public && !target.public { + log::warn!( + "Rejecting connection from public interface to private bind: {bind} -> {target:?}" + ); + return Ok(()); + } + let peek = db.peek().await; + let root = peek + .as_private() + .as_key_store() + .as_local_certs() + .as_root_cert() + .de()?; + let mut cfg = async { + if let Some((domain, provider, settings)) = + target_name.as_ref().and_then(|domain| { + target.acme.as_ref().and_then(|a| { + peek.as_public() + .as_server_info() + .as_acme() + .as_idx(a) + .map(|s| (domain, a, s)) + }) + }) + { + let acme_settings = settings.de()?; + let mut identifiers = vec![Identifier::Dns(domain.to_string())]; + if false + // Requires RFC 8738 + { + if let Some(wan_ip) = wan_ip { + identifiers.push(Identifier::Ip(wan_ip.into())); + } + } + let (send, recv) = watch::channel(None); + acme_tls_alpn_cache.mutate(|c| c.insert(domain.clone(), recv)); + let cert = async_acme::rustls_helper::order( + |_, cert| { + send.send_replace(Some(Arc::new(cert))); + Ok(()) + }, + provider.0.as_str(), + &identifiers, + Some(&AcmeCertCache(&db)), + &acme_settings.contact, + ) + .await + .with_kind(ErrorKind::OpenSsl)?; + return Ok(ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert))))); + } + + let hostnames = target_name + .into_iter() + .chain([InternedString::from_display(&bind.ip())]) + .chain(wan_ip.as_ref().map(InternedString::from_display)) + .collect(); + let key = db + .mutate(|v| { + v.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth(); + if mid + .client_hello() + .signature_schemes() + .contains(&tokio_rustls::rustls::SignatureScheme::ED25519) + { + cfg.with_single_cert( + key.fullchain_ed25519() + .into_iter() + .map(|c| { + Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( + c.to_der()?, + )) + }) + .collect::>()?, + PrivateKeyDer::from(PrivatePkcs8KeyDer::from( + key.leaf.keys.ed25519.private_key_to_pkcs8()?, + )), + ) + } else { + cfg.with_single_cert( + key.fullchain_nistp256() + .into_iter() + .map(|c| { + Ok(tokio_rustls::rustls::pki_types::CertificateDer::from( + c.to_der()?, + )) + }) + .collect::>()?, + PrivateKeyDer::from(PrivatePkcs8KeyDer::from( + key.leaf.keys.nistp256.private_key_to_pkcs8()?, + )), + ) + } + .with_kind(crate::ErrorKind::OpenSsl) + } + .await?; + let mut tcp_stream = TcpStream::connect(target.addr).await?; + match target.connect_ssl { + Ok(()) => { + let mut client_cfg = + tokio_rustls::rustls::ClientConfig::builder_with_provider(crypto_provider) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_root_certificates({ + let mut store = RootCertStore::empty(); + store + .add(CertificateDer::from(root.to_der()?)) + .with_kind(crate::ErrorKind::OpenSsl)?; + store + }) + .with_no_client_auth(); + client_cfg.alpn_protocols = mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .map(|x| x.to_vec()) + .collect(); + let mut target_stream = TlsConnector::from(Arc::new(client_cfg)) + .connect_with( + ServerName::IpAddress(target.addr.ip().into()), + tcp_stream, + |conn| { + cfg.alpn_protocols + .extend(conn.alpn_protocol().into_iter().map(|p| p.to_vec())) + }, + ) + .await + .with_kind(crate::ErrorKind::OpenSsl)?; + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, + Err(e) => { + tracing::trace!( + "VHostController: failed to accept TLS connection on {bind}: {e}" + ); + tracing::trace!("{e:?}"); + return Ok(()); + } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut target_stream).await + } + Err(AlpnInfo::Reflect) => { + for proto in mid.client_hello().alpn().into_iter().flatten() { + cfg.alpn_protocols.push(proto.into()); + } + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, + Err(e) => { + tracing::trace!( + "VHostController: failed to accept TLS connection on {bind}: {e}" + ); + tracing::trace!("{e:?}"); + return Ok(()); } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut tcp_stream).await + } + Err(AlpnInfo::Specified(alpn)) => { + cfg.alpn_protocols = alpn.into_iter().map(|a| a.0).collect(); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + let mut tls_stream = match accept.await { + Ok(a) => a, Err(e) => { tracing::trace!( - "VHostController: failed to accept connection on port {port}: {e}" + "VHostController: failed to accept TLS connection on {bind}: {e}" ); tracing::trace!("{e:?}"); + return Ok(()); } + }; + tokio::io::copy_bidirectional(&mut tls_stream, &mut tcp_stream).await + } + } + .map_or_else( + |e| { + use std::io::ErrorKind as E; + match e.kind() { + E::UnexpectedEof + | E::BrokenPipe + | E::ConnectionAborted + | E::ConnectionReset + | E::ConnectionRefused + | E::TimedOut + | E::Interrupted + | E::NotConnected => Ok(()), + _ => Err(e), + } + }, + |_| Ok(()), + )?; + } else { + // 503 + } + Ok::<_, Error>(()) + } + + #[instrument(skip_all)] + fn new( + port: u16, + db: TypedPatchDb, + iface_ctrl: Arc, + crypto_provider: Arc, + acme_tls_alpn_cache: AcmeTlsAlpnCache, + ) -> Result { + let mut listener = iface_ctrl.bind(port).with_kind(crate::ErrorKind::Network)?; + let (map_send, map_recv) = watch::channel(BTreeMap::new()); + Ok(Self { + mapping: map_send, + _thread: tokio::spawn(async move { + loop { + if let Err(e) = Self::accept( + &mut listener, + map_recv.clone(), + db.clone(), + acme_tls_alpn_cache.clone(), + crypto_provider.clone(), + ) + .await + { + tracing::error!( + "VHostController: failed to accept connection on {port}: {e}" + ); + tracing::debug!("{e:?}"); } } }) .into(), }) } - async fn add(&self, hostname: Option, target: TargetInfo) -> Result, Error> { - if let Some(mapping) = Weak::upgrade(&self.mapping) { - let mut writable = mapping.write().await; + fn add(&self, hostname: Option, target: TargetInfo) -> Result, Error> { + let mut res = Ok(Arc::new(())); + self.mapping.send_if_modified(|writable| { + let mut changed = false; let mut targets = writable.remove(&hostname).unwrap_or_default(); let rc = if let Some(rc) = Weak::upgrade(&targets.remove(&target).unwrap_or_default()) { rc } else { + changed = true; Arc::new(()) }; targets.insert(target, Arc::downgrade(&rc)); writable.insert(hostname, targets); - Ok(rc) + res = Ok(rc); + changed + }); + if !self.mapping.is_closed() { + res } else { Err(Error::new( eyre!("VHost Service Thread has exited"), @@ -380,33 +719,22 @@ impl VHostServer { )) } } - async fn gc(&self, hostname: Option) -> Result<(), Error> { - if let Some(mapping) = Weak::upgrade(&self.mapping) { - let mut writable = mapping.write().await; + fn gc(&self, hostname: Option) { + self.mapping.send_if_modified(|writable| { let mut targets = writable.remove(&hostname).unwrap_or_default(); + let pre = targets.len(); targets = targets .into_iter() .filter(|(_, rc)| rc.strong_count() > 0) .collect(); + let post = targets.len(); if !targets.is_empty() { writable.insert(hostname, targets); } - Ok(()) - } else { - Err(Error::new( - eyre!("VHost Service Thread has exited"), - crate::ErrorKind::Network, - )) - } + pre == post + }); } - async fn is_empty(&self) -> Result { - if let Some(mapping) = Weak::upgrade(&self.mapping) { - Ok(mapping.read().await.is_empty()) - } else { - Err(Error::new( - eyre!("VHost Service Thread has exited"), - crate::ErrorKind::Network, - )) - } + fn is_empty(&self) -> bool { + self.mapping.borrow().is_empty() } } diff --git a/core/startos/src/net/web_server.rs b/core/startos/src/net/web_server.rs index c2e25a413..641a6f08d 100644 --- a/core/startos/src/net/web_server.rs +++ b/core/startos/src/net/web_server.rs @@ -1,41 +1,279 @@ -use std::convert::Infallible; +use std::future::Future; use std::net::SocketAddr; +use std::ops::Deref; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, RwLock}; +use std::task::Poll; +use std::time::Duration; -use futures::future::ready; +use axum::Router; +use futures::future::Either; use futures::FutureExt; use helpers::NonDetachingJoinHandle; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Server; +use hyper_util::rt::{TokioIo, TokioTimer}; +use tokio::net::{TcpListener, TcpStream}; use tokio::sync::oneshot; -use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; +use crate::net::network_interface::{ + NetworkInterfaceListener, SelfContainedNetworkInterfaceListener, +}; use crate::net::static_server::{ - diag_ui_file_router, install_ui_file_router, main_ui_server_router, setup_ui_file_router, + diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, redirecter, refresher, + setup_ui_router, }; -use crate::net::HttpHandler; -use crate::Error; +use crate::prelude::*; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::sync::Watch; + +pub struct Accepted { + pub https_redirect: bool, + pub stream: TcpStream, +} + +pub trait Accept { + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll>; +} + +impl Accept for Vec { + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + for listener in &*self { + if let Poll::Ready((stream, _)) = listener.poll_accept(cx)? { + return Poll::Ready(Ok(Accepted { + https_redirect: false, + stream, + })); + } + } + Poll::Pending + } +} +impl Accept for NetworkInterfaceListener { + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + NetworkInterfaceListener::poll_accept(self, cx, true).map(|res| { + res.map(|a| Accepted { + https_redirect: a.is_public, + stream: a.stream, + }) + }) + } +} + +impl Accept for Either { + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + match self { + Either::Left(a) => a.poll_accept(cx), + Either::Right(b) => b.poll_accept(cx), + } + } +} +impl Accept for Option { + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + match self { + None => Poll::Pending, + Some(a) => a.poll_accept(cx), + } + } +} + +#[pin_project::pin_project] +pub struct Acceptor { + acceptor: Watch, +} +impl Acceptor { + pub fn new(acceptor: A) -> Self { + Self { + acceptor: Watch::new(acceptor), + } + } + + fn poll_changed(&mut self, cx: &mut std::task::Context<'_>) -> Poll<()> { + self.acceptor.poll_changed(cx) + } + + fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + let _ = self.poll_changed(cx); + self.acceptor.peek_mut(|a| a.poll_accept(cx)) + } -pub struct WebServer { + async fn accept(&mut self) -> Result { + std::future::poll_fn(|cx| self.poll_accept(cx)).await + } +} +impl Acceptor> { + pub async fn bind(listen: impl IntoIterator) -> Result { + Ok(Self::new( + futures::future::try_join_all(listen.into_iter().map(TcpListener::bind)).await?, + )) + } +} + +pub type UpgradableListener = + Option>; + +impl Acceptor { + pub fn bind_upgradable(listener: SelfContainedNetworkInterfaceListener) -> Self { + Self::new(Some(Either::Left(listener))) + } +} + +pub struct WebServerAcceptorSetter { + acceptor: Watch, +} +impl WebServerAcceptorSetter>> { + pub fn try_upgrade Result>(&self, f: F) -> Result<(), Error> { + let mut res = Ok(()); + self.acceptor.send_modify(|a| { + *a = match a.take() { + Some(Either::Left(a)) => match f(a) { + Ok(b) => Some(Either::Right(b)), + Err(e) => { + res = Err(e); + None + } + }, + x => x, + } + }); + res + } +} +impl Deref for WebServerAcceptorSetter { + type Target = Watch; + fn deref(&self) -> &Self::Target { + &self.acceptor + } +} + +pub struct WebServer { shutdown: oneshot::Sender<()>, + router: Watch>, + acceptor: Watch, thread: NonDetachingJoinHandle<()>, } -impl WebServer { - pub fn new(bind: SocketAddr, router: HttpHandler) -> Self { +impl WebServer { + pub fn acceptor_setter(&self) -> WebServerAcceptorSetter { + WebServerAcceptorSetter { + acceptor: self.acceptor.clone(), + } + } + + pub fn new(mut acceptor: Acceptor) -> Self { + let acceptor_send = acceptor.acceptor.clone(); + let router = Watch::>::new(None); + let service = router.clone_unseen(); let (shutdown, shutdown_recv) = oneshot::channel(); let thread = NonDetachingJoinHandle::from(tokio::spawn(async move { - let server = Server::bind(&bind) - .http1_preserve_header_case(true) - .http1_title_case_headers(true) - .serve(make_service_fn(move |_| { - let router = router.clone(); - ready(Ok::<_, Infallible>(service_fn(move |req| router(req)))) - })) - .with_graceful_shutdown(shutdown_recv.map(|_| ())); - if let Err(e) = server.await { - tracing::error!("Spawning hyper server error: {}", e); + #[derive(Clone)] + struct QueueRunner { + queue: Arc>>, + } + impl hyper::rt::Executor for QueueRunner + where + Fut: Future + Send + 'static, + { + fn execute(&self, fut: Fut) { + if let Some(q) = &*self.queue.read().unwrap() { + q.add_job(fut); + } else { + tracing::warn!("job queued after shutdown"); + } + } + } + + struct SwappableRouter(Watch>, bool); + impl hyper::service::Service> for SwappableRouter { + type Response = , + >>::Response; + type Error = , + >>::Error; + type Future = , + >>::Future; + + fn call(&self, req: hyper::Request) -> Self::Future { + use tower_service::Service; + + if self.1 { + redirecter().call(req) + } else { + let router = self.0.read(); + if let Some(mut router) = router { + router.call(req) + } else { + refresher().call(req) + } + } + } + } + + let accept = AtomicBool::new(true); + let queue_cell = Arc::new(RwLock::new(None)); + let graceful = hyper_util::server::graceful::GracefulShutdown::new(); + let mut server = hyper_util::server::conn::auto::Builder::new(QueueRunner { + queue: queue_cell.clone(), + }); + server + .http1() + .timer(TokioTimer::new()) + .title_case_headers(true) + .preserve_header_case(true) + .http2() + .timer(TokioTimer::new()) + .enable_connect_protocol() + .keep_alive_interval(Duration::from_secs(60)) + .keep_alive_timeout(Duration::from_secs(300)); + let (queue, mut runner) = BackgroundJobQueue::new(); + *queue_cell.write().unwrap() = Some(queue.clone()); + + let handler = async { + loop { + if let Err(e) = async { + let accepted = acceptor.accept().await?; + queue.add_job( + graceful.watch( + server + .serve_connection_with_upgrades( + TokioIo::new(accepted.stream), + SwappableRouter(service.clone(), accepted.https_redirect), + ) + .into_owned(), + ), + ); + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error accepting HTTP connection: {e}"); + tracing::debug!("{e:?}"); + } + } + } + .boxed(); + + tokio::select! { + _ = shutdown_recv => (), + _ = handler => (), + _ = &mut runner => (), + } + + accept.store(false, std::sync::atomic::Ordering::SeqCst); + drop(queue); + drop(queue_cell.write().unwrap().take()); + + if !runner.is_empty() { + runner.await; } })); - Self { shutdown, thread } + Self { + shutdown, + router, + thread, + acceptor: acceptor_send, + } } pub async fn shutdown(self) { @@ -43,19 +281,27 @@ impl WebServer { self.thread.await.unwrap() } - pub async fn main(bind: SocketAddr, ctx: RpcContext) -> Result { - Ok(Self::new(bind, main_ui_server_router(ctx).await?)) + pub fn serve_router(&mut self, router: Router) { + self.router.send(Some(router)) + } + + pub fn serve_main(&mut self, ctx: RpcContext) { + self.serve_router(main_ui_router(ctx)) + } + + pub fn serve_setup(&mut self, ctx: SetupContext) { + self.serve_router(setup_ui_router(ctx)) } - pub async fn setup(bind: SocketAddr, ctx: SetupContext) -> Result { - Ok(Self::new(bind, setup_ui_file_router(ctx).await?)) + pub fn serve_diagnostic(&mut self, ctx: DiagnosticContext) { + self.serve_router(diagnostic_ui_router(ctx)) } - pub async fn diagnostic(bind: SocketAddr, ctx: DiagnosticContext) -> Result { - Ok(Self::new(bind, diag_ui_file_router(ctx).await?)) + pub fn serve_install(&mut self, ctx: InstallContext) { + self.serve_router(install_ui_router(ctx)) } - pub async fn install(bind: SocketAddr, ctx: InstallContext) -> Result { - Ok(Self::new(bind, install_ui_file_router(ctx).await?)) + pub fn serve_init(&mut self, ctx: InitContext) { + self.serve_router(init_ui_router(ctx)) } } diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index 8429f9205..9b858aafe 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -3,19 +3,25 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; -use clap::ArgMatches; +use clap::builder::TypedValueParser; +use clap::Parser; use isocountry::CountryCode; use lazy_static::lazy_static; use regex::Regex; -use rpc_toolkit::command; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use tokio::sync::RwLock; use tracing::instrument; +use ts_rs::TS; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::WifiInfo; +use crate::db::model::Database; +use crate::net::utils::find_wifi_iface; use crate::prelude::*; -use crate::util::serde::{display_serializable, IoFormat}; -use crate::util::{display_none, Invoke}; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; +use crate::util::Invoke; use crate::{Error, ErrorKind}; type WifiManager = Arc>; @@ -31,28 +37,79 @@ pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { } } -#[command(subcommands(add, connect, delete, get, country, available))] -pub async fn wifi() -> Result<(), Error> { - Ok(()) +pub fn wifi() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add) + .no_display() + .with_about("Add wifi ssid and password") + .with_call_remote::(), + ) + .subcommand( + "connect", + from_fn_async(connect) + .no_display() + .with_about("Connect to wifi network") + .with_call_remote::(), + ) + .subcommand( + "delete", + from_fn_async(delete) + .no_display() + .with_about("Remove a wifi network") + .with_call_remote::(), + ) + .subcommand( + "get", + from_fn_async(get) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_wifi_info(handle.params, result)) + }) + .with_about("List wifi info") + .with_call_remote::(), + ) + .subcommand( + "country", + country::().with_about("Command to set country"), + ) + .subcommand( + "available", + available::().with_about("Command to list available wifi networks"), + ) } -#[command(subcommands(get_available))] -pub async fn available() -> Result<(), Error> { - Ok(()) +pub fn available() -> ParentHandler { + ParentHandler::new().subcommand( + "get", + from_fn_async(get_available) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_wifi_list(handle.params, result))) + .with_about("List available wifi networks") + .with_call_remote::(), + ) } -#[command(subcommands(set_country))] -pub async fn country() -> Result<(), Error> { - Ok(()) +pub fn country() -> ParentHandler { + ParentHandler::new().subcommand( + "set", + from_fn_async(set_country) + .no_display() + .with_about("Set Country") + .with_call_remote::(), + ) } -#[command(display(display_none))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct AddParams { + ssid: String, + password: String, +} #[instrument(skip_all)] -pub async fn add( - #[context] ctx: RpcContext, - #[arg] ssid: String, - #[arg] password: String, -) -> Result<(), Error> { +pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Result<(), Error> { let wifi_manager = wifi_manager(&ctx)?; if !ssid.is_ascii() { return Err(Error::new( @@ -67,7 +124,7 @@ pub async fn add( )); } async fn add_procedure( - db: PatchDb, + db: TypedPatchDb, wifi_manager: WifiManager, ssid: &Ssid, password: &Psk, @@ -93,12 +150,29 @@ pub async fn add( ErrorKind::Wifi, )); } + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_wifi_mut() + .as_ssids_mut() + .mutate(|s| { + s.insert(ssid); + Ok(()) + }) + }) + .await?; Ok(()) } +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct SsidParams { + ssid: String, +} -#[command(display(display_none))] #[instrument(skip_all)] -pub async fn connect(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result<(), Error> { +pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { let wifi_manager = wifi_manager(&ctx)?; if !ssid.is_ascii() { return Err(Error::new( @@ -107,7 +181,7 @@ pub async fn connect(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result< )); } async fn connect_procedure( - db: PatchDb, + db: TypedPatchDb, wifi_manager: WifiManager, ssid: &Ssid, ) -> Result<(), Error> { @@ -141,12 +215,22 @@ pub async fn connect(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result< ErrorKind::Wifi, )); } + + ctx.db + .mutate(|db| { + let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + wifi.as_ssids_mut().mutate(|s| { + s.insert(ssid.clone()); + Ok(()) + })?; + wifi.as_selected_mut().ser(&Some(ssid)) + }) + .await?; Ok(()) } -#[command(display(display_none))] #[instrument(skip_all)] -pub async fn delete(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result<(), Error> { +pub async fn delete(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { let wifi_manager = wifi_manager(&ctx)?; if !ssid.is_ascii() { return Err(Error::new( @@ -167,11 +251,23 @@ pub async fn delete(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result<( } wpa_supplicant.remove_network(ctx.db.clone(), &ssid).await?; + + ctx.db + .mutate(|db| { + let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + wifi.as_ssids_mut().mutate(|s| { + s.remove(&ssid.0); + Ok(()) + })?; + wifi.as_selected_mut() + .map_mutate(|s| Ok(s.filter(|s| s == &ssid.0))) + }) + .await?; Ok(()) } #[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct WiFiInfo { +#[serde(rename_all = "camelCase")] +pub struct WifiListInfo { ssids: HashMap, connected: Option, country: Option, @@ -179,30 +275,30 @@ pub struct WiFiInfo { available_wifi: Vec, } #[derive(serde::Serialize, serde::Deserialize, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct WifiListInfo { +#[serde(rename_all = "camelCase")] +pub struct WifiListInfoLow { strength: SignalStrength, security: Vec, } #[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct WifiListOut { ssid: Ssid, strength: SignalStrength, security: Vec, } -pub type WifiList = HashMap; -fn display_wifi_info(info: WiFiInfo, matches: &ArgMatches) { +pub type WifiList = HashMap; +fn display_wifi_info(params: WithIoFormat, info: WifiListInfo) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(info, matches); + if let Some(format) = params.format { + return display_serializable(format, info); } let mut table_global = Table::new(); table_global.add_row(row![bc => "CONNECTED", - "SIGNAL_STRENGTH", + "SIGNAL STRENGTH", "COUNTRY", "ETHERNET", ]); @@ -210,12 +306,12 @@ fn display_wifi_info(info: WiFiInfo, matches: &ArgMatches) { &info .connected .as_ref() - .map_or("[N/A]".to_owned(), |c| c.0.clone()), + .map_or("N/A".to_owned(), |c| c.0.clone()), &info .connected .as_ref() .and_then(|x| info.ssids.get(x)) - .map_or("[N/A]".to_owned(), |ss| format!("{}", ss.0)), + .map_or("N/A".to_owned(), |ss| format!("{}", ss.0)), info.country.as_ref().map(|c| c.alpha2()).unwrap_or("00"), &format!("{}", info.ethernet) ]); @@ -256,11 +352,11 @@ fn display_wifi_info(info: WiFiInfo, matches: &ArgMatches) { table_global.print_tty(false).unwrap(); } -fn display_wifi_list(info: Vec, matches: &ArgMatches) { +fn display_wifi_list(params: WithIoFormat, info: Vec) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(info, matches); + if let Some(format) = params.format { + return display_serializable(format, info); } let mut table_global = Table::new(); @@ -280,14 +376,9 @@ fn display_wifi_list(info: Vec, matches: &ArgMatches) { table_global.print_tty(false).unwrap(); } -#[command(display(display_wifi_info))] +// #[command(display(display_wifi_info))] #[instrument(skip_all)] -pub async fn get( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { +pub async fn get(ctx: RpcContext, _: Empty) -> Result { let wifi_manager = wifi_manager(&ctx)?; let wpa_supplicant = wifi_manager.read().await; let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!( @@ -325,7 +416,7 @@ pub async fn get( }) .collect(); let current = current_res?; - Ok(WiFiInfo { + Ok(WifiListInfo { ssids, connected: current, country: country_res?, @@ -334,14 +425,8 @@ pub async fn get( }) } -#[command(rename = "get", display(display_wifi_list))] #[instrument(skip_all)] -pub async fn get_available( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result, Error> { +pub async fn get_available(ctx: RpcContext, _: Empty) -> Result, Error> { let wifi_manager = wifi_manager(&ctx)?; let wpa_supplicant = wifi_manager.read().await; let (wifi_list, network_list) = tokio::join!( @@ -366,10 +451,17 @@ pub async fn get_available( Ok(wifi_list) } -#[command(rename = "set", display(display_none))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct SetCountryParams { + #[arg(value_parser = CountryCodeParser)] + #[ts(type = "string")] + country: CountryCode, +} pub async fn set_country( - #[context] ctx: RpcContext, - #[arg(parse(country_code_parse))] country: CountryCode, + ctx: RpcContext, + SetCountryParams { country }: SetCountryParams, ) -> Result<(), Error> { let wifi_manager = wifi_manager(&ctx)?; if !interface_connected(&ctx.ethernet_interface).await? { @@ -433,7 +525,7 @@ impl SignalStrength { } #[derive(Debug, Clone)] -pub struct WifiInfo { +pub struct WifiInfoLow { ssid: Ssid, device: Option, } @@ -560,7 +652,7 @@ impl WpaCli { Ok(()) } #[instrument(skip_all)] - pub async fn list_networks_low(&self) -> Result, Error> { + pub async fn list_networks_low(&self) -> Result, Error> { let r = Command::new("nmcli") .arg("-t") .arg("c") @@ -579,13 +671,13 @@ impl WpaCli { if !connection_type.contains("wireless") { return None; } - let info = WifiInfo { + let info = WifiInfoLow { ssid: name, device: device.map(|x| x.to_owned()), }; Some((uuid, info)) }) - .collect::>()) + .collect::>()) } #[instrument(skip_all)] @@ -608,7 +700,7 @@ impl WpaCli { values.next()?.split(' ').map(|x| x.to_owned()).collect(); Some(( ssid, - WifiListInfo { + WifiListInfoLow { strength: signal, security, }, @@ -637,11 +729,13 @@ impl WpaCli { Ok(()) } - pub async fn save_config(&mut self, db: PatchDb) -> Result<(), Error> { + pub async fn save_config(&mut self, db: TypedPatchDb) -> Result<(), Error> { let new_country = self.get_country_low().await?; db.mutate(|d| { - d.as_server_info_mut() - .as_last_wifi_region_mut() + d.as_public_mut() + .as_server_info_mut() + .as_wifi_mut() + .as_last_region_mut() .ser(&new_country) }) .await @@ -675,7 +769,11 @@ impl WpaCli { .collect()) } #[instrument(skip_all)] - pub async fn select_network(&mut self, db: PatchDb, ssid: &Ssid) -> Result { + pub async fn select_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + ) -> Result { let m_id = self.check_active_network(ssid).await?; match m_id { None => Err(Error::new( @@ -727,7 +825,11 @@ impl WpaCli { } } #[instrument(skip_all)] - pub async fn remove_network(&mut self, db: PatchDb, ssid: &Ssid) -> Result { + pub async fn remove_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + ) -> Result { let found_networks = self.find_networks(ssid).await?; if found_networks.is_empty() { return Ok(true); @@ -741,7 +843,7 @@ impl WpaCli { #[instrument(skip_all)] pub async fn set_add_network( &mut self, - db: PatchDb, + db: TypedPatchDb, ssid: &Ssid, psk: &Psk, ) -> Result<(), Error> { @@ -750,7 +852,12 @@ impl WpaCli { Ok(()) } #[instrument(skip_all)] - pub async fn add_network(&mut self, db: PatchDb, ssid: &Ssid, psk: &Psk) -> Result<(), Error> { + pub async fn add_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + psk: &Psk, + ) -> Result<(), Error> { self.add_network_low(ssid, psk).await?; self.save_config(db).await?; Ok(()) @@ -769,45 +876,55 @@ pub async fn interface_connected(interface: &str) -> Result { Ok(v.is_some()) } -pub fn country_code_parse(code: &str, _matches: &ArgMatches) -> Result { - CountryCode::for_alpha2(code).map_err(|_| { - Error::new( - color_eyre::eyre::eyre!("Invalid Country Code: {}", code), - ErrorKind::Wifi, - ) - }) +#[derive(Clone)] +struct CountryCodeParser; +impl TypedValueParser for CountryCodeParser { + type Value = CountryCode; + fn parse_ref( + &self, + _: &clap::Command, + _: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let code = value.to_string_lossy(); + CountryCode::for_alpha2(&code).map_err(|_| { + clap::Error::raw( + clap::error::ErrorKind::ValueValidation, + color_eyre::eyre::eyre!("Invalid Country Code: {}", code), + ) + }) + } } #[instrument(skip_all)] -pub async fn synchronize_wpa_supplicant_conf>( +pub async fn synchronize_network_manager>( main_datadir: P, - wifi_iface: &str, - last_country_code: &Option, + wifi: &WifiInfo, ) -> Result<(), Error> { let persistent = main_datadir.as_ref().join("system-connections"); - tracing::debug!("persistent: {:?}", persistent); - // let supplicant = Path::new("/etc/wpa_supplicant.conf"); if tokio::fs::metadata(&persistent).await.is_err() { tokio::fs::create_dir_all(&persistent).await?; } crate::disk::mount::util::bind(&persistent, "/etc/NetworkManager/system-connections", false) .await?; - // if tokio::fs::metadata(&supplicant).await.is_err() { - // tokio::fs::write(&supplicant, include_str!("wpa_supplicant.conf.base")).await?; - // } Command::new("systemctl") .arg("restart") .arg("NetworkManager") .invoke(ErrorKind::Wifi) .await?; + + let Some(wifi_iface) = &wifi.interface else { + return Ok(()); + }; + Command::new("ifconfig") .arg(wifi_iface) .arg("up") .invoke(ErrorKind::Wifi) .await?; - if let Some(last_country_code) = last_country_code { + if let Some(last_country_code) = wifi.last_region { tracing::info!("Setting the region"); let _ = Command::new("iw") .arg("reg") diff --git a/core/startos/src/net/ws_server.rs b/core/startos/src/net/ws_server.rs deleted file mode 100644 index 16519c6c8..000000000 --- a/core/startos/src/net/ws_server.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::context::RpcContext; - -pub async fn ws_server_handle(rpc_ctx: RpcContext) { - - let ws_ctx = rpc_ctx.clone(); - let ws_server_handle = { - let builder = Server::bind(&ws_ctx.bind_ws); - - let make_svc = ::rpc_toolkit::hyper::service::make_service_fn(move |_| { - let ctx = ws_ctx.clone(); - async move { - Ok::<_, ::rpc_toolkit::hyper::Error>(::rpc_toolkit::hyper::service::service_fn( - move |req| { - let ctx = ctx.clone(); - async move { - tracing::debug!("Request to {}", req.uri().path()); - match req.uri().path() { - "/ws/db" => { - Ok(subscribe(ctx, req).await.unwrap_or_else(err_to_500)) - } - path if path.starts_with("/ws/rpc/") => { - match RequestGuid::from( - path.strip_prefix("/ws/rpc/").unwrap(), - ) { - None => { - tracing::debug!("No Guid Path"); - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::empty()) - } - Some(guid) => { - match ctx.get_ws_continuation_handler(&guid).await { - Some(cont) => match cont(req).await { - Ok(r) => Ok(r), - Err(e) => Response::builder() - .status( - StatusCode::INTERNAL_SERVER_ERROR, - ) - .body(Body::from(format!("{}", e))), - }, - _ => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - } - } - } - } - path if path.starts_with("/rest/rpc/") => { - match RequestGuid::from( - path.strip_prefix("/rest/rpc/").unwrap(), - ) { - None => { - tracing::debug!("No Guid Path"); - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::empty()) - } - Some(guid) => { - match ctx.get_rest_continuation_handler(&guid).await - { - None => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - Some(cont) => match cont(req).await { - Ok(r) => Ok(r), - Err(e) => Response::builder() - .status( - StatusCode::INTERNAL_SERVER_ERROR, - ) - .body(Body::from(format!("{}", e))), - }, - } - } - } - } - _ => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()), - } - } - }, - )) - } - }); - builder.serve(make_svc) - } - .with_graceful_shutdown({ - let mut shutdown = rpc_ctx.shutdown.subscribe(); - async move { - shutdown.recv().await.expect("context dropped"); - } - }); - -} \ No newline at end of file diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index 73351471c..19376026d 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -1,151 +1,189 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use chrono::{DateTime, Utc}; +use clap::builder::ValueParserFactory; +use clap::Parser; use color_eyre::eyre::eyre; -use rpc_toolkit::command; -use sqlx::PgPool; -use tokio::sync::Mutex; +use imbl_value::InternedString; +use models::{FromStrParser, PackageId}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tracing::instrument; +use ts_rs::TS; use crate::backup::BackupReport; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; +use crate::db::model::{Database, DatabaseModel}; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::display_none; -use crate::util::serde::display_serializable; -use crate::{Error, ErrorKind, ResultExt}; +use crate::util::serde::HandlerExtSerde; -#[command(subcommands(list, delete, delete_before, create))] -pub async fn notification() -> Result<(), Error> { - Ok(()) +// #[command(subcommands(list, delete, delete_before, create))] +pub fn notification() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list) + .with_display_serializable() + .with_about("List notifications") + .with_call_remote::(), + ) + .subcommand( + "delete", + from_fn_async(delete) + .no_display() + .with_about("Delete notification for a given id") + .with_call_remote::(), + ) + .subcommand( + "delete-before", + from_fn_async(delete_before) + .no_display() + .with_about("Delete notifications preceding a given id") + .with_call_remote::(), + ) + .subcommand( + "create", + from_fn_async(create) + .no_display() + .with_about("Persist a newly created notification") + .with_call_remote::(), + ) } -#[command(display(display_serializable))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ListParams { + #[ts(type = "number | null")] + before: Option, + #[ts(type = "number | null")] + limit: Option, +} +// #[command(display(display_serializable))] #[instrument(skip_all)] pub async fn list( - #[context] ctx: RpcContext, - #[arg] before: Option, - #[arg] limit: Option, -) -> Result, Error> { - let limit = limit.unwrap_or(40); - match before { - None => { - let records = sqlx::query!( - "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications ORDER BY id DESC LIMIT $1", - limit as i64 - ).fetch_all(&ctx.secret_store).await?; - let notifs = records - .into_iter() - .map(|r| { - Ok(Notification { - id: r.id as u32, - package_id: r.package_id.and_then(|p| p.parse().ok()), - created_at: DateTime::from_utc(r.created_at, Utc), - code: r.code as u32, - level: match r.level.parse::() { - Ok(a) => a, - Err(e) => return Err(e.into()), - }, - title: r.title, - message: r.message, - data: match r.data { - None => serde_json::Value::Null, - Some(v) => match v.parse::() { - Ok(a) => a, - Err(e) => { - return Err(Error::new( - eyre!("Invalid Notification Data: {}", e), - ErrorKind::ParseDbField, - )) - } - }, - }, - }) - }) - .collect::, Error>>()?; - - ctx.db - .mutate(|d| { - d.as_server_info_mut() + ctx: RpcContext, + ListParams { before, limit }: ListParams, +) -> Result, Error> { + ctx.db + .mutate(|db| { + let limit = limit.unwrap_or(40); + match before { + None => { + let records = db + .as_private() + .as_notifications() + .as_entries()? + .into_iter() + .rev() + .take(limit); + let notifs = records + .into_iter() + .map(|(id, notification)| { + Ok(NotificationWithId { + id, + notification: notification.de()?, + }) + }) + .collect::, Error>>()?; + db.as_public_mut() + .as_server_info_mut() .as_unread_notification_count_mut() - .ser(&0) - }) - .await?; - Ok(notifs) - } - Some(before) => { - let records = sqlx::query!( - "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications WHERE id < $1 ORDER BY id DESC LIMIT $2", - before, - limit as i64 - ).fetch_all(&ctx.secret_store).await?; - let res = records - .into_iter() - .map(|r| { - Ok(Notification { - id: r.id as u32, - package_id: r.package_id.and_then(|p| p.parse().ok()), - created_at: DateTime::from_utc(r.created_at, Utc), - code: r.code as u32, - level: match r.level.parse::() { - Ok(a) => a, - Err(e) => return Err(e.into()), - }, - title: r.title, - message: r.message, - data: match r.data { - None => serde_json::Value::Null, - Some(v) => match v.parse::() { - Ok(a) => a, - Err(e) => { - return Err(Error::new( - eyre!("Invalid Notification Data: {}", e), - ErrorKind::ParseDbField, - )) - } - }, - }, - }) - }) - .collect::, Error>>()?; - Ok(res) - } - } + .ser(&0)?; + Ok(notifs) + } + Some(before) => { + let records = db + .as_private() + .as_notifications() + .as_entries()? + .into_iter() + .filter(|(id, _)| *id < before) + .rev() + .take(limit); + records + .into_iter() + .map(|(id, notification)| { + Ok(NotificationWithId { + id, + notification: notification.de()?, + }) + }) + .collect() + } + } + }) + .await } -#[command(display(display_none))] -pub async fn delete(#[context] ctx: RpcContext, #[arg] id: i32) -> Result<(), Error> { - sqlx::query!("DELETE FROM notifications WHERE id = $1", id) - .execute(&ctx.secret_store) - .await?; - Ok(()) +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DeleteParams { + #[ts(type = "number")] + id: u32, } -#[command(rename = "delete-before", display(display_none))] -pub async fn delete_before(#[context] ctx: RpcContext, #[arg] before: i32) -> Result<(), Error> { - sqlx::query!("DELETE FROM notifications WHERE id < $1", before) - .execute(&ctx.secret_store) - .await?; - Ok(()) +pub async fn delete(ctx: RpcContext, DeleteParams { id }: DeleteParams) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_private_mut().as_notifications_mut().remove(&id)?; + Ok(()) + }) + .await +} +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DeleteBeforeParams { + #[ts(type = "number")] + before: u32, +} + +pub async fn delete_before( + ctx: RpcContext, + DeleteBeforeParams { before }: DeleteBeforeParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + for id in db.as_private().as_notifications().keys()? { + if id < before { + db.as_private_mut().as_notifications_mut().remove(&id)?; + } + } + Ok(()) + }) + .await +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CreateParams { + package: Option, + level: NotificationLevel, + title: String, + message: String, } -#[command(display(display_none))] pub async fn create( - #[context] ctx: RpcContext, - #[arg] package: Option, - #[arg] level: NotificationLevel, - #[arg] title: String, - #[arg] message: String, + ctx: RpcContext, + CreateParams { + package, + level, + title, + message, + }: CreateParams, ) -> Result<(), Error> { - ctx.notification_manager - .notify(ctx.db.clone(), package, level, title, message, (), None) + ctx.db + .mutate(|db| notify(db, package, level, title, message, ())) .await } -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] pub enum NotificationLevel { Success, Info, @@ -162,6 +200,13 @@ impl fmt::Display for NotificationLevel { } } } +impl ValueParserFactory for NotificationLevel { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + pub struct InvalidNotificationLevel(String); impl From for crate::Error { fn from(val: InvalidNotificationLevel) -> Self { @@ -188,115 +233,98 @@ impl fmt::Display for InvalidNotificationLevel { write!(f, "Invalid Notification Level: {}", self.0) } } -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Notifications(pub BTreeMap); +impl Notifications { + pub fn new() -> Self { + Self(BTreeMap::new()) + } +} +impl Map for Notifications { + type Key = u32; + type Value = Notification; + fn key_str(key: &Self::Key) -> Result, Error> { + Self::key_string(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::from_display(key)) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Notification { + pub package_id: Option, + pub created_at: DateTime, + pub code: u32, + pub level: NotificationLevel, + pub title: String, + pub message: String, + pub data: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotificationWithId { id: u32, - package_id: Option, // TODO change for package id newtype - created_at: DateTime, - code: u32, - level: NotificationLevel, - title: String, - message: String, - data: serde_json::Value, + #[serde(flatten)] + notification: Notification, } pub trait NotificationType: serde::Serialize + for<'de> serde::Deserialize<'de> + std::fmt::Debug { - const CODE: i32; + const CODE: u32; } impl NotificationType for () { - const CODE: i32 = 0; + const CODE: u32 = 0; } impl NotificationType for BackupReport { - const CODE: i32 = 1; + const CODE: u32 = 1; } - -pub struct NotificationManager { - sqlite: PgPool, - cache: Mutex, NotificationLevel, String), i64>>, +impl NotificationType for String { + const CODE: u32 = 2; } -impl NotificationManager { - pub fn new(sqlite: PgPool) -> Self { - NotificationManager { - sqlite, - cache: Mutex::new(HashMap::new()), - } - } - #[instrument(skip(db, subtype, self))] - pub async fn notify( - &self, - db: PatchDb, - package_id: Option, - level: NotificationLevel, - title: String, - message: String, - subtype: T, - debounce_interval: Option, - ) -> Result<(), Error> { - let peek = db.peek().await; - if !self - .should_notify(&package_id, &level, &title, debounce_interval) - .await - { - return Ok(()); - } - let mut count = peek.as_server_info().as_unread_notification_count().de()?; - let sql_package_id = package_id.as_ref().map(|p| &**p); - let sql_code = T::CODE; - let sql_level = format!("{}", level); - let sql_data = - serde_json::to_string(&subtype).with_kind(crate::ErrorKind::Serialization)?; - sqlx::query!( - "INSERT INTO notifications (package_id, code, level, title, message, data) VALUES ($1, $2, $3, $4, $5, $6)", - sql_package_id, - sql_code as i32, - sql_level, - title, - message, - sql_data - ).execute(&self.sqlite).await?; - count += 1; - db.mutate(|db| { - db.as_server_info_mut() - .as_unread_notification_count_mut() - .ser(&count) - }) - .await - } - async fn should_notify( - &self, - package_id: &Option, - level: &NotificationLevel, - title: &String, - debounce_interval: Option, - ) -> bool { - let mut guard = self.cache.lock().await; - let k = (package_id.clone(), level.clone(), title.clone()); - let v = (*guard).get(&k); - match v { - None => { - (*guard).insert(k, Utc::now().timestamp()); - true - } - Some(last_issued) => match debounce_interval { - None => { - (*guard).insert(k, Utc::now().timestamp()); - true - } - Some(interval) => { - if last_issued + interval as i64 > Utc::now().timestamp() { - false - } else { - (*guard).insert(k, Utc::now().timestamp()); - true - } - } - }, - } - } + +#[instrument(skip(subtype, db))] +pub fn notify( + db: &mut DatabaseModel, + package_id: Option, + level: NotificationLevel, + title: String, + message: String, + subtype: T, +) -> Result<(), Error> { + let data = to_value(&subtype)?; + db.as_public_mut() + .as_server_info_mut() + .as_unread_notification_count_mut() + .mutate(|c| { + *c += 1; + Ok(()) + })?; + let id = db + .as_private() + .as_notifications() + .keys()? + .into_iter() + .max() + .map_or(0, |id| id + 1); + db.as_private_mut().as_notifications_mut().insert( + &id, + &Notification { + package_id, + created_at: Utc::now(), + code: T::CODE, + level, + title, + message, + data, + }, + ) } #[test] diff --git a/core/startos/src/os_install/gpt.rs b/core/startos/src/os_install/gpt.rs index 4139b4cf2..4833f4ea7 100644 --- a/core/startos/src/os_install/gpt.rs +++ b/core/startos/src/os_install/gpt.rs @@ -50,7 +50,7 @@ pub async fn partition(disk: &DiskInfo, overwrite: bool) -> Result Result gpt::partition_types::LINUX_ROOT_X64, "aarch64" => gpt::partition_types::LINUX_ROOT_ARM_64, _ => gpt::partition_types::LINUX_FS, diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 9e21e9f23..4cd8aac2d 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -1,47 +1,64 @@ use std::path::{Path, PathBuf}; +use clap::Parser; use color_eyre::eyre::eyre; use models::Error; -use rpc_toolkit::command; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::process::Command; +use ts_rs::TS; -use crate::context::InstallContext; +use crate::context::config::ServerConfig; +use crate::context::{CliContext, InstallContext}; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::efivarfs::EfiVarFs; +use crate::disk::mount::filesystem::overlayfs::OverlayFs; use crate::disk::mount::filesystem::{MountType, ReadWrite}; -use crate::disk::mount::guard::{MountGuard, TmpMountGuard}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::util::{DiskInfo, PartitionTable}; use crate::disk::OsPartitionInfo; -use crate::net::utils::{find_eth_iface, find_wifi_iface}; +use crate::net::utils::find_eth_iface; +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::util::io::{delete_file, open_file, TmpDir}; use crate::util::serde::IoFormat; -use crate::util::{display_none, Invoke}; +use crate::util::Invoke; use crate::ARCH; mod gpt; mod mbr; -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct PostInstallConfig { - os_partitions: OsPartitionInfo, - ethernet_interface: String, - wifi_interface: Option, -} - -#[command(subcommands(disk, execute, reboot))] -pub fn install() -> Result<(), Error> { - Ok(()) +pub fn install() -> ParentHandler { + ParentHandler::new() + .subcommand("disk", disk::().with_about("Command to list disk info")) + .subcommand( + "execute", + from_fn_async(execute::) + .no_display() + .with_about("Install StartOS over existing version") + .with_call_remote::(), + ) + .subcommand( + "reboot", + from_fn_async(reboot) + .no_display() + .with_about("Restart the server") + .with_call_remote::(), + ) } -#[command(subcommands(list))] -pub fn disk() -> Result<(), Error> { - Ok(()) +pub fn disk() -> ParentHandler { + ParentHandler::new().subcommand( + "list", + from_fn_async(list) + .no_display() + .with_about("List disk info") + .with_call_remote::(), + ) } -#[command(display(display_none))] -pub async fn list() -> Result, Error> { +pub async fn list(_: InstallContext) -> Result, Error> { let skip = match async { Ok::<_, Error>( Path::new( @@ -103,10 +120,21 @@ async fn partition(disk: &mut DiskInfo, overwrite: bool) -> Result( + _: C, + ExecuteParams { + logicalname, + mut overwrite, + }: ExecuteParams, ) -> Result<(), Error> { let mut disk = crate::disk::util::list(&Default::default()) .await? @@ -119,7 +147,6 @@ pub async fn execute( ) })?; let eth_iface = find_eth_iface().await?; - let wifi_iface = find_wifi_iface().await?; overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none()); @@ -153,21 +180,12 @@ pub async fn execute( { if let Err(e) = async { // cp -r ${guard}/config /tmp/config - if tokio::fs::metadata(guard.as_ref().join("config/upgrade")) - .await - .is_ok() - { - tokio::fs::remove_file(guard.as_ref().join("config/upgrade")).await?; - } - if tokio::fs::metadata(guard.as_ref().join("config/disk.guid")) - .await - .is_ok() - { - tokio::fs::remove_file(guard.as_ref().join("config/disk.guid")).await?; - } + delete_file(guard.path().join("config/upgrade")).await?; + delete_file(guard.path().join("config/overlay/etc/hostname")).await?; + delete_file(guard.path().join("config/disk.guid")).await?; Command::new("cp") .arg("-r") - .arg(guard.as_ref().join("config")) + .arg(guard.path().join("config")) .arg("/tmp/config.bak") .invoke(crate::ErrorKind::Filesystem) .await?; @@ -195,57 +213,120 @@ pub async fn execute( .arg("rootfs") .invoke(crate::ErrorKind::DiskManagement) .await?; - let rootfs = TmpMountGuard::mount(&BlockDev::new(&part_info.root), ReadWrite).await?; + + let config_path = rootfs.path().join("config"); + if tokio::fs::metadata("/tmp/config.bak").await.is_ok() { + if tokio::fs::metadata(&config_path).await.is_ok() { + tokio::fs::remove_dir_all(&config_path).await?; + } Command::new("cp") .arg("-r") .arg("/tmp/config.bak") - .arg(rootfs.as_ref().join("config")) + .arg(&config_path) .invoke(crate::ErrorKind::Filesystem) .await?; } else { - tokio::fs::create_dir(rootfs.as_ref().join("config")).await?; + tokio::fs::create_dir_all(&config_path).await?; } - tokio::fs::create_dir(rootfs.as_ref().join("next")).await?; - let current = rootfs.as_ref().join("current"); - tokio::fs::create_dir(¤t).await?; - tokio::fs::create_dir(current.join("boot")).await?; - let boot = MountGuard::mount( + let images_path = rootfs.path().join("images"); + tokio::fs::create_dir_all(&images_path).await?; + let image_path = images_path + .join(hex::encode( + &MultiCursorFile::from(open_file("/run/live/medium/live/filesystem.squashfs").await?) + .blake3_mmap() + .await? + .as_bytes()[..16], + )) + .with_extension("rootfs"); + tokio::fs::copy("/run/live/medium/live/filesystem.squashfs", &image_path).await?; + // TODO: check hash of fs + let unsquash_target = TmpDir::new().await?; + let bootfs = MountGuard::mount( &BlockDev::new(&part_info.boot), - current.join("boot"), + unsquash_target.join("boot"), ReadWrite, ) .await?; - - let efi = if let Some(efi) = &part_info.efi { - Some(MountGuard::mount(&BlockDev::new(efi), current.join("boot/efi"), ReadWrite).await?) - } else { - None - }; - Command::new("unsquashfs") .arg("-n") .arg("-f") .arg("-d") - .arg(¤t) + .arg(&*unsquash_target) .arg("/run/live/medium/live/filesystem.squashfs") + .arg("boot") .invoke(crate::ErrorKind::Filesystem) .await?; + bootfs.unmount(true).await?; + unsquash_target.delete().await?; + Command::new("ln") + .arg("-rsf") + .arg(&image_path) + .arg(config_path.join("current.rootfs")) + .invoke(ErrorKind::DiskManagement) + .await?; tokio::fs::write( - rootfs.as_ref().join("config/config.yaml"), - IoFormat::Yaml.to_vec(&PostInstallConfig { - os_partitions: part_info.clone(), - ethernet_interface: eth_iface, - wifi_interface: wifi_iface, + rootfs.path().join("config/config.yaml"), + IoFormat::Yaml.to_vec(&ServerConfig { + os_partitions: Some(part_info.clone()), + ethernet_interface: Some(eth_iface), + ..Default::default() })?, ) .await?; + let lower = TmpMountGuard::mount(&BlockDev::new(&image_path), MountType::ReadOnly).await?; + let work = config_path.join("work"); + let upper = config_path.join("overlay"); + let overlay = + TmpMountGuard::mount(&OverlayFs::new(&lower.path(), &upper, &work), ReadWrite).await?; + + let boot = MountGuard::mount( + &BlockDev::new(&part_info.boot), + overlay.path().join("boot"), + ReadWrite, + ) + .await?; + let efi = if let Some(efi) = &part_info.efi { + Some( + MountGuard::mount( + &BlockDev::new(efi), + overlay.path().join("boot/efi"), + ReadWrite, + ) + .await?, + ) + } else { + None + }; + let start_os_fs = MountGuard::mount( + &Bind::new(rootfs.path()), + overlay.path().join("media/startos/root"), + MountType::ReadOnly, + ) + .await?; + let dev = MountGuard::mount(&Bind::new("/dev"), overlay.path().join("dev"), ReadWrite).await?; + let proc = + MountGuard::mount(&Bind::new("/proc"), overlay.path().join("proc"), ReadWrite).await?; + let sys = MountGuard::mount(&Bind::new("/sys"), overlay.path().join("sys"), ReadWrite).await?; + let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { + Some( + MountGuard::mount( + &EfiVarFs, + overlay.path().join("sys/firmware/efi/efivars"), + ReadWrite, + ) + .await?, + ) + } else { + None + }; + tokio::fs::write( - current.join("etc/fstab"), + overlay.path().join("etc/fstab"), format!( include_str!("fstab.template"), boot = part_info.boot.display(), @@ -260,46 +341,24 @@ pub async fn execute( .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("systemd-machine-id-setup") .invoke(crate::ErrorKind::Systemd) .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("ssh-keygen") .arg("-A") .invoke(crate::ErrorKind::OpenSsh) .await?; - let embassy_fs = MountGuard::mount( - &Bind::new(rootfs.as_ref()), - current.join("media/embassy/embassyfs"), - MountType::ReadOnly, - ) - .await?; - let dev = MountGuard::mount(&Bind::new("/dev"), current.join("dev"), ReadWrite).await?; - let proc = MountGuard::mount(&Bind::new("/proc"), current.join("proc"), ReadWrite).await?; - let sys = MountGuard::mount(&Bind::new("/sys"), current.join("sys"), ReadWrite).await?; - let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { - Some( - MountGuard::mount( - &EfiVarFs, - current.join("sys/firmware/efi/efivars"), - ReadWrite, - ) - .await?, - ) - } else { - None - }; - let mut install = Command::new("chroot"); - install.arg(¤t).arg("grub-install"); + install.arg(overlay.path()).arg("grub-install"); if tokio::fs::metadata("/sys/firmware/efi").await.is_err() { install.arg("--target=i386-pc"); } else { - match *ARCH { + match ARCH { "x86_64" => install.arg("--target=x86_64-efi"), "aarch64" => install.arg("--target=arm64-efi"), _ => &mut install, @@ -311,7 +370,7 @@ pub async fn execute( .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("update-grub2") .invoke(crate::ErrorKind::Grub) .await?; @@ -321,17 +380,22 @@ pub async fn execute( } sys.unmount(false).await?; proc.unmount(false).await?; - embassy_fs.unmount(false).await?; + start_os_fs.unmount(false).await?; if let Some(efi) = efi { efi.unmount(false).await?; } boot.unmount(false).await?; + + overlay.unmount().await?; + tokio::fs::remove_dir_all(&work).await?; + lower.unmount().await?; + rootfs.unmount().await?; + Ok(()) } -#[command(display(display_none))] -pub async fn reboot(#[context] ctx: InstallContext) -> Result<(), Error> { +pub async fn reboot(ctx: InstallContext) -> Result<(), Error> { Command::new("sync") .invoke(crate::ErrorKind::Filesystem) .await?; diff --git a/core/startos/src/prelude.rs b/core/startos/src/prelude.rs index ab5de1d38..702d77c2d 100644 --- a/core/startos/src/prelude.rs +++ b/core/startos/src/prelude.rs @@ -1,6 +1,25 @@ pub use color_eyre::eyre::eyre; +pub use lazy_format::lazy_format; pub use models::OptionExt; +pub use tracing::instrument; pub use crate::db::prelude::*; pub use crate::ensure_code; pub use crate::error::{Error, ErrorCollection, ErrorKind, ResultExt}; + +#[macro_export] +macro_rules! dbg { + () => {{ + tracing::debug!("[{}:{}:{}]", file!(), line!(), column!()); + }}; + ($e:expr) => {{ + let e = $e; + tracing::debug!("[{}:{}:{}] {} = {e:?}", file!(), line!(), column!(), stringify!($e)); + e + }}; + ($($e:expr),+) => { + ($( + crate::dbg!($e) + ),+) + } +} diff --git a/core/startos/src/procedure/docker.rs b/core/startos/src/procedure/docker.rs deleted file mode 100644 index ad25953a3..000000000 --- a/core/startos/src/procedure/docker.rs +++ /dev/null @@ -1,970 +0,0 @@ -use std::borrow::Cow; -use std::collections::{BTreeMap, BTreeSet, VecDeque}; -use std::ffi::{OsStr, OsString}; -use std::net::Ipv4Addr; -use std::os::unix::prelude::FileTypeExt; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use futures::future::{BoxFuture, Either as EitherFuture}; -use futures::{FutureExt, TryStreamExt}; -use helpers::{NonDetachingJoinHandle, UnixRpcClient}; -use models::{Id, ImageId, SYSTEM_PACKAGE_ID}; -use nix::sys::signal; -use nix::unistd::Pid; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::io::{AsyncBufRead, AsyncBufReadExt, BufReader}; -use tokio::time::timeout; -use tracing::instrument; - -use super::ProcedureName; -use crate::context::RpcContext; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::docker::{remove_container, CONTAINER_TOOL}; -use crate::util::serde::{Duration as SerdeDuration, IoFormat}; -use crate::util::Version; -use crate::volume::{VolumeId, Volumes}; -use crate::{Error, ResultExt, HOST_IP}; - -pub const NET_TLD: &str = "embassy"; - -lazy_static::lazy_static! { - pub static ref SYSTEM_IMAGES: BTreeSet = { - let mut set = BTreeSet::new(); - - set.insert("compat".parse().unwrap()); - set.insert("utils".parse().unwrap()); - - set - }; -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct DockerContainers { - pub main: DockerContainer, - // #[serde(default)] - // pub aux: BTreeMap, -} - -/// This is like the docker procedures of the past designs, -/// but this time all the entrypoints and args are not -/// part of this struct by choice. Used for the times that we are creating our own entry points -#[derive(Clone, Debug, Deserialize, Serialize, patch_db::HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct DockerContainer { - pub image: ImageId, - #[serde(default)] - pub mounts: BTreeMap, - #[serde(default)] - pub shm_size_mb: Option, // TODO: use postfix sizing? like 1k vs 1m vs 1g - #[serde(default)] - pub sigterm_timeout: Option, - #[serde(default)] - pub system: bool, - #[serde(default)] - pub gpu_acceleration: bool, -} - -impl DockerContainer { - /// We created a new exec runner, where we are going to be passing the commands for it to run. - /// Idea is that we are going to send it command and get the inputs be filtered back from the manager. - /// Then we could in theory run commands without the cost of running the docker exec which is known to have - /// a dely of > 200ms which is not acceptable. - #[instrument(skip_all)] - pub async fn long_running_execute( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result<(LongRunning, UnixRpcClient), Error> { - let container_name = DockerProcedure::container_name(pkg_id, None); - - let socket_path = - Path::new("/tmp/embassy/containers").join(format!("{pkg_id}_{pkg_version}")); - if tokio::fs::metadata(&socket_path).await.is_ok() { - tokio::fs::remove_dir_all(&socket_path).await?; - } - tokio::fs::create_dir_all(&socket_path).await?; - - let mut cmd = LongRunning::setup_long_running_docker_cmd( - self, - ctx, - &container_name, - volumes, - pkg_id, - pkg_version, - &socket_path, - ) - .await?; - - let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; - - let client = UnixRpcClient::new(socket_path.join("rpc.sock")); - - let running_output = NonDetachingJoinHandle::from(tokio::spawn(async move { - if let Err(err) = handle - .wait() - .await - .map_err(|e| eyre!("Runtime error: {e:?}")) - { - tracing::error!("{}", err); - tracing::debug!("{:?}", err); - } - })); - - { - let socket = socket_path.join("rpc.sock"); - if let Err(_err) = timeout(Duration::from_secs(1), async move { - while tokio::fs::metadata(&socket).await.is_err() { - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - { - tracing::error!("Timed out waiting for init to create socket"); - } - } - - Ok((LongRunning { running_output }, client)) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct DockerProcedure { - pub image: ImageId, - #[serde(default)] - pub system: bool, - pub entrypoint: String, - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub inject: bool, - #[serde(default)] - pub mounts: BTreeMap, - #[serde(default)] - pub io_format: Option, - #[serde(default)] - pub sigterm_timeout: Option, - #[serde(default)] - pub shm_size_mb: Option, // TODO: use postfix sizing? like 1k vs 1m vs 1g - #[serde(default)] - pub gpu_acceleration: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Default)] -#[serde(rename_all = "kebab-case")] -pub struct DockerInject { - #[serde(default)] - pub system: bool, - pub entrypoint: String, - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub io_format: Option, - #[serde(default)] - pub sigterm_timeout: Option, -} -impl DockerProcedure { - pub fn main_docker_procedure( - container: &DockerContainer, - injectable: &DockerInject, - ) -> DockerProcedure { - DockerProcedure { - image: container.image.clone(), - system: injectable.system, - entrypoint: injectable.entrypoint.clone(), - args: injectable.args.clone(), - inject: false, - mounts: container.mounts.clone(), - io_format: injectable.io_format, - sigterm_timeout: injectable.sigterm_timeout, - shm_size_mb: container.shm_size_mb, - gpu_acceleration: container.gpu_acceleration, - } - } - - pub fn validate( - &self, - _eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - expected_io: bool, - ) -> Result<(), color_eyre::eyre::Report> { - for volume in self.mounts.keys() { - if !volumes.contains_key(volume) && !matches!(&volume, &VolumeId::Backup) { - color_eyre::eyre::bail!("unknown volume: {}", volume); - } - } - if self.system { - if !SYSTEM_IMAGES.contains(&self.image) { - color_eyre::eyre::bail!("unknown system image: {}", self.image); - } - } else if !image_ids.contains(&self.image) { - color_eyre::eyre::bail!("image for {} not contained in package", self.image); - } - if expected_io && self.io_format.is_none() { - color_eyre::eyre::bail!("expected io-format"); - } - Ok(()) - } - - #[instrument(skip_all)] - pub async fn execute( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - name: ProcedureName, - volumes: &Volumes, - input: Option, - timeout: Option, - ) -> Result, Error> { - let name = name.docker_name(); - let name: Option<&str> = name.as_deref(); - let mut cmd = tokio::process::Command::new(CONTAINER_TOOL); - let container_name = Self::container_name(pkg_id, name); - cmd.arg("run") - .arg("--rm") - .arg("--network=start9") - .arg(format!("--add-host=embassy:{}", Ipv4Addr::from(HOST_IP))) - .arg("--name") - .arg(&container_name) - .arg(format!("--hostname={}", &container_name)) - .arg("--no-healthcheck") - .kill_on_drop(true); - remove_container(&container_name, true).await?; - cmd.args(self.docker_args(ctx, pkg_id, pkg_version, volumes).await?); - let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) { - cmd.stdin(std::process::Stdio::piped()); - Some(format.to_vec(input)?) - } else { - None - }; - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - tracing::trace!( - "{}", - format!("{:?}", cmd) - .split(r#"" ""#) - .collect::>() - .join(" ") - ); - let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; - let id = handle.id(); - let timeout_fut = if let Some(timeout) = timeout { - EitherFuture::Right(async move { - tokio::time::sleep(timeout).await; - - Ok(()) - }) - } else { - EitherFuture::Left(futures::future::pending::>()) - }; - if let (Some(input), Some(mut stdin)) = (&input_buf, handle.stdin.take()) { - use tokio::io::AsyncWriteExt; - stdin - .write_all(input) - .await - .with_kind(crate::ErrorKind::Docker)?; - stdin.flush().await?; - stdin.shutdown().await?; - drop(stdin); - } - enum Race { - Done(T), - TimedOut, - } - - let io_format = self.io_format; - let mut output = BufReader::new( - handle - .stdout - .take() - .ok_or_else(|| eyre!("Can't takeout stdout in execute")) - .with_kind(crate::ErrorKind::Docker)?, - ); - let output = NonDetachingJoinHandle::from(tokio::spawn(async move { - match async { - if let Some(format) = io_format { - return match max_by_lines(&mut output, None).await { - MaxByLines::Done(buffer) => { - Ok::( - match format.from_slice(buffer.as_bytes()) { - Ok(a) => a, - Err(e) => { - tracing::trace!( - "Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.", - format, - e - ); - Value::String(buffer) - } - }, - ) - }, - MaxByLines::Error(e) => Err(e), - MaxByLines::Overflow(buffer) => Ok(Value::String(buffer)) - } - } - - let lines = buf_reader_to_lines(&mut output, 1000).await?; - if lines.is_empty() { - return Ok(Value::Null); - } - - let joined_output = lines.join("\n"); - Ok(Value::String(joined_output)) - }.await { - Ok(a) => Ok((a, output)), - Err(e) => Err((e, output)) - } - })); - let err_output = BufReader::new( - handle - .stderr - .take() - .ok_or_else(|| eyre!("Can't takeout std err")) - .with_kind(crate::ErrorKind::Docker)?, - ); - - let err_output = NonDetachingJoinHandle::from(tokio::spawn(async move { - let lines = buf_reader_to_lines(err_output, 1000).await?; - let joined_output = lines.join("\n"); - Ok::<_, Error>(joined_output) - })); - - let res = tokio::select! { - res = handle.wait() => Race::Done(res.with_kind(crate::ErrorKind::Docker)?), - res = timeout_fut => { - res?; - Race::TimedOut - }, - }; - let exit_status = match res { - Race::Done(x) => x, - Race::TimedOut => { - if let Some(id) = id { - signal::kill(Pid::from_raw(id as i32), signal::SIGKILL) - .with_kind(crate::ErrorKind::Docker)?; - } - return Ok(Err((143, "Timed out. Retrying soon...".to_owned()))); - } - }; - Ok( - if exit_status.success() || exit_status.code() == Some(143) { - Ok(serde_json::from_value( - output - .await - .with_kind(crate::ErrorKind::Unknown)? - .map(|(v, _)| v) - .map_err(|(e, _)| tracing::warn!("{}", e)) - .unwrap_or_default(), - ) - .with_kind(crate::ErrorKind::Deserialization)?) - } else { - Err(( - exit_status.code().unwrap_or_default(), - err_output.await.with_kind(crate::ErrorKind::Unknown)??, - )) - }, - ) - } - - #[instrument(skip_all)] - pub async fn inject( - &self, - _ctx: &RpcContext, - pkg_id: &PackageId, - _pkg_version: &Version, - _name: ProcedureName, - _volumes: &Volumes, - input: Option, - timeout: Option, - ) -> Result, Error> { - let mut cmd = tokio::process::Command::new(CONTAINER_TOOL); - - cmd.arg("exec"); - - cmd.args(self.docker_args_inject(pkg_id)); - let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) { - cmd.stdin(std::process::Stdio::piped()); - Some(format.to_vec(input)?) - } else { - None - }; - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - tracing::trace!( - "{}", - format!("{:?}", cmd) - .split(r#"" ""#) - .collect::>() - .join(" ") - ); - let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; - let id = handle.id(); - let timeout_fut = if let Some(timeout) = timeout { - EitherFuture::Right(async move { - tokio::time::sleep(timeout).await; - - Ok(()) - }) - } else { - EitherFuture::Left(futures::future::pending::>()) - }; - if let (Some(input), Some(mut stdin)) = (&input_buf, handle.stdin.take()) { - use tokio::io::AsyncWriteExt; - stdin - .write_all(input) - .await - .with_kind(crate::ErrorKind::Docker)?; - stdin.flush().await?; - stdin.shutdown().await?; - drop(stdin); - } - enum Race { - Done(T), - TimedOut, - } - - let io_format = self.io_format; - let mut output = BufReader::new( - handle - .stdout - .take() - .ok_or_else(|| eyre!("Can't takeout stdout in inject")) - .with_kind(crate::ErrorKind::Docker)?, - ); - let output = NonDetachingJoinHandle::from(tokio::spawn(async move { - match async { - if let Some(format) = io_format { - return match max_by_lines(&mut output, None).await { - MaxByLines::Done(buffer) => { - Ok::( - match format.from_slice(buffer.as_bytes()) { - Ok(a) => a, - Err(e) => { - tracing::trace!( - "Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.", - format, - e - ); - Value::String(buffer) - } - }, - ) - }, - MaxByLines::Error(e) => Err(e), - MaxByLines::Overflow(buffer) => Ok(Value::String(buffer)) - } - } - - let lines = buf_reader_to_lines(&mut output, 1000).await?; - if lines.is_empty() { - return Ok(Value::Null); - } - - let joined_output = lines.join("\n"); - Ok(Value::String(joined_output)) - }.await { - Ok(a) => Ok((a, output)), - Err(e) => Err((e, output)) - } - })); - let err_output = BufReader::new( - handle - .stderr - .take() - .ok_or_else(|| eyre!("Can't takeout std err")) - .with_kind(crate::ErrorKind::Docker)?, - ); - - let err_output = NonDetachingJoinHandle::from(tokio::spawn(async move { - let lines = buf_reader_to_lines(err_output, 1000).await?; - let joined_output = lines.join("\n"); - Ok::<_, Error>(joined_output) - })); - - let res = tokio::select! { - res = handle.wait() => Race::Done(res.with_kind(crate::ErrorKind::Docker)?), - res = timeout_fut => { - res?; - Race::TimedOut - }, - }; - let exit_status = match res { - Race::Done(x) => x, - Race::TimedOut => { - if let Some(id) = id { - signal::kill(Pid::from_raw(id as i32), signal::SIGKILL) - .with_kind(crate::ErrorKind::Docker)?; - } - return Ok(Err((143, "Timed out. Retrying soon...".to_owned()))); - } - }; - Ok( - if exit_status.success() || exit_status.code() == Some(143) { - Ok(serde_json::from_value( - output - .await - .with_kind(crate::ErrorKind::Unknown)? - .map(|(v, _)| v) - .map_err(|(e, _)| tracing::warn!("{}", e)) - .unwrap_or_default(), - ) - .with_kind(crate::ErrorKind::Deserialization)?) - } else { - Err(( - exit_status.code().unwrap_or_default(), - err_output.await.with_kind(crate::ErrorKind::Unknown)??, - )) - }, - ) - } - - #[instrument(skip_all)] - pub async fn sandboxed( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - input: Option, - timeout: Option, - ) -> Result, Error> { - let mut cmd = tokio::process::Command::new(CONTAINER_TOOL); - cmd.arg("run").arg("--rm").arg("--network=none"); - cmd.args( - self.docker_args(ctx, pkg_id, pkg_version, &volumes.to_readonly()) - .await?, - ); - let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) { - cmd.stdin(std::process::Stdio::piped()); - Some(format.to_vec(input)?) - } else { - None - }; - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; - if let (Some(input), Some(stdin)) = (&input_buf, &mut handle.stdin) { - use tokio::io::AsyncWriteExt; - stdin - .write_all(input) - .await - .with_kind(crate::ErrorKind::Docker)?; - } - - let err_output = BufReader::new( - handle - .stderr - .take() - .ok_or_else(|| eyre!("Can't takeout std err")) - .with_kind(crate::ErrorKind::Docker)?, - ); - let err_output = NonDetachingJoinHandle::from(tokio::spawn(async move { - let lines = buf_reader_to_lines(err_output, 1000).await?; - let joined_output = lines.join("\n"); - Ok::<_, Error>(joined_output) - })); - - let io_format = self.io_format; - let mut output = BufReader::new( - handle - .stdout - .take() - .ok_or_else(|| eyre!("Can't takeout stdout in sandboxed")) - .with_kind(crate::ErrorKind::Docker)?, - ); - let output = NonDetachingJoinHandle::from(tokio::spawn(async move { - match async { - if let Some(format) = io_format { - return match max_by_lines(&mut output, None).await { - MaxByLines::Done(buffer) => { - Ok::( - match format.from_slice(buffer.as_bytes()) { - Ok(a) => a, - Err(e) => { - tracing::trace!( - "Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.", - format, - e - ); - Value::String(buffer) - } - }, - ) - }, - MaxByLines::Error(e) => Err(e), - MaxByLines::Overflow(buffer) => Ok(Value::String(buffer)) - } - } - - let lines = buf_reader_to_lines(&mut output, 1000).await?; - if lines.is_empty() { - return Ok(Value::Null); - } - - let joined_output = lines.join("\n"); - Ok(Value::String(joined_output)) - }.await { - Ok(a) => Ok((a, output)), - Err(e) => Err((e, output)) - } - })); - - let handle = if let Some(dur) = timeout { - async move { - tokio::time::timeout(dur, handle.wait()) - .await - .with_kind(crate::ErrorKind::Docker)? - .with_kind(crate::ErrorKind::Docker) - } - .boxed() - } else { - async { handle.wait().await.with_kind(crate::ErrorKind::Docker) }.boxed() - }; - let exit_status = handle.await?; - Ok( - if exit_status.success() || exit_status.code() == Some(143) { - Ok(serde_json::from_value( - output - .await - .with_kind(crate::ErrorKind::Unknown)? - .map(|(v, _)| v) - .map_err(|(e, _)| tracing::warn!("{}", e)) - .unwrap_or_default(), - ) - .with_kind(crate::ErrorKind::Deserialization)?) - } else { - Err(( - exit_status.code().unwrap_or_default(), - err_output.await.with_kind(crate::ErrorKind::Unknown)??, - )) - }, - ) - } - - pub fn container_name(pkg_id: &PackageId, name: Option<&str>) -> String { - if let Some(name) = name { - format!("{}_{}.{}", pkg_id, name, NET_TLD) - } else { - format!("{}.{}", pkg_id, NET_TLD) - } - } - - pub fn uncontainer_name(name: &str) -> Option<(PackageId, Option<&str>)> { - let (pre_tld, _) = name.split_once('.')?; - if pre_tld.contains('_') { - let (pkg, name) = name.split_once('_')?; - Some((Id::try_from(pkg).ok()?.into(), Some(name))) - } else { - Some((Id::try_from(pre_tld).ok()?.into(), None)) - } - } - - async fn docker_args( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result>, Error> { - let mut res = self.new_docker_args(); - for (volume_id, dst) in &self.mounts { - let volume = if let Some(v) = volumes.get(volume_id) { - v - } else { - continue; - }; - let src = volume.path_for(&ctx.datadir, pkg_id, pkg_version, volume_id); - if let Err(_e) = tokio::fs::metadata(&src).await { - tokio::fs::create_dir_all(&src).await?; - } - res.push(OsStr::new("--mount").into()); - res.push( - OsString::from(format!( - "type=bind,src={},dst={}{}", - src.display(), - dst.display(), - if volume.readonly() { ",readonly" } else { "" } - )) - .into(), - ); - } - if let Some(shm_size_mb) = self.shm_size_mb { - res.push(OsStr::new("--shm-size").into()); - res.push(OsString::from(format!("{}m", shm_size_mb)).into()); - } - if self.gpu_acceleration { - fn get_devices<'a>( - path: &'a Path, - res: &'a mut Vec, - ) -> BoxFuture<'a, Result<(), Error>> { - async move { - let mut read_dir = tokio::fs::read_dir(path).await?; - while let Some(entry) = read_dir.next_entry().await? { - let fty = entry.metadata().await?.file_type(); - if fty.is_block_device() || fty.is_char_device() { - res.push(entry.path()); - } else if fty.is_dir() { - get_devices(&entry.path(), res).await?; - } - } - Ok(()) - } - .boxed() - } - let mut devices = Vec::new(); - get_devices(Path::new("/dev/dri"), &mut devices).await?; - for device in devices { - res.push(OsStr::new("--device").into()); - res.push(OsString::from(device).into()); - } - } - res.push(OsStr::new("--interactive").into()); - res.push(OsStr::new("--log-driver=journald").into()); - res.push(OsStr::new("--entrypoint").into()); - res.push(OsStr::new(&self.entrypoint).into()); - if self.system { - res.push(OsString::from(self.image.for_package(&SYSTEM_PACKAGE_ID, None)).into()); - } else { - res.push(OsString::from(self.image.for_package(pkg_id, Some(pkg_version))).into()); - } - - res.extend(self.args.iter().map(|s| OsStr::new(s).into())); - - Ok(res) - } - - fn new_docker_args(&self) -> Vec> { - Vec::with_capacity( - (2 * self.mounts.len()) // --mount - + (2 * self.shm_size_mb.is_some() as usize) // --shm-size - + 5 // --interactive --log-driver=journald --entrypoint - + self.args.len(), // [ARG...] - ) - } - fn docker_args_inject(&self, pkg_id: &PackageId) -> Vec> { - let mut res = self.new_docker_args(); - if let Some(shm_size_mb) = self.shm_size_mb { - res.push(OsStr::new("--shm-size").into()); - res.push(OsString::from(format!("{}m", shm_size_mb)).into()); - } - res.push(OsStr::new("--interactive").into()); - - res.push(OsString::from(Self::container_name(pkg_id, None)).into()); - res.push(OsStr::new(&self.entrypoint).into()); - - res.extend(self.args.iter().map(|s| OsStr::new(s).into())); - - res - } -} - -struct RingVec { - value: VecDeque, - capacity: usize, -} -impl RingVec { - fn new(capacity: usize) -> Self { - RingVec { - value: VecDeque::with_capacity(capacity), - capacity, - } - } - fn push(&mut self, item: T) -> Option { - let popped_item = if self.value.len() == self.capacity { - self.value.pop_front() - } else { - None - }; - self.value.push_back(item); - popped_item - } -} - -/// This is created when we wanted a long running docker executor that we could send commands to and get the responses back. -/// We wanted a long running since we want to be able to have the equivelent to the docker execute without the heavy costs of 400 + ms time lag. -/// Also the long running let's us have the ability to start/ end the services quicker. -pub struct LongRunning { - pub running_output: NonDetachingJoinHandle<()>, -} - -impl LongRunning { - async fn setup_long_running_docker_cmd( - docker: &DockerContainer, - ctx: &RpcContext, - container_name: &str, - volumes: &Volumes, - pkg_id: &PackageId, - pkg_version: &Version, - socket_path: &Path, - ) -> Result { - const INIT_EXEC: &str = "/start9/bin/container-init"; - const BIND_LOCATION: &str = "/usr/lib/startos/container/"; - tracing::trace!("setup_long_running_docker_cmd"); - - remove_container(container_name, true).await?; - - let image_architecture = { - let mut cmd = tokio::process::Command::new(CONTAINER_TOOL); - cmd.arg("image") - .arg("inspect") - .arg("--format") - .arg("'{{.Architecture}}'"); - - if docker.system { - cmd.arg(docker.image.for_package(&SYSTEM_PACKAGE_ID, None)); - } else { - cmd.arg(docker.image.for_package(pkg_id, Some(pkg_version))); - } - let arch = String::from_utf8(cmd.output().await?.stdout)?; - arch.replace('\'', "").trim().to_string() - }; - - let mut cmd = tokio::process::Command::new(CONTAINER_TOOL); - cmd.arg("run") - .arg("--network=start9") - .arg(format!("--add-host=embassy:{}", Ipv4Addr::from(HOST_IP))) - .arg("--mount") - .arg(format!( - "type=bind,src={BIND_LOCATION},dst=/start9/bin/,readonly" - )) - .arg("--mount") - .arg(format!( - "type=bind,src={input},dst=/start9/sockets/", - input = socket_path.display() - )) - .arg("--name") - .arg(container_name) - .arg(format!("--hostname={}", &container_name)) - .arg("--entrypoint") - .arg(format!("{INIT_EXEC}.{image_architecture}")) - .arg("-i") - .arg("--rm") - .kill_on_drop(true); - - for (volume_id, dst) in &docker.mounts { - let volume = if let Some(v) = volumes.get(volume_id) { - v - } else { - continue; - }; - let src = volume.path_for(&ctx.datadir, pkg_id, pkg_version, volume_id); - if let Err(_e) = tokio::fs::metadata(&src).await { - tokio::fs::create_dir_all(&src).await?; - } - cmd.arg("--mount").arg(format!( - "type=bind,src={},dst={}{}", - src.display(), - dst.display(), - if volume.readonly() { ",readonly" } else { "" } - )); - } - if let Some(shm_size_mb) = docker.shm_size_mb { - cmd.arg("--shm-size").arg(format!("{}m", shm_size_mb)); - } - cmd.arg("--log-driver=journald"); - if docker.system { - cmd.arg(docker.image.for_package(&SYSTEM_PACKAGE_ID, None)); - } else { - cmd.arg(docker.image.for_package(pkg_id, Some(pkg_version))); - } - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::inherit()); - cmd.stdin(std::process::Stdio::piped()); - Ok(cmd) - } -} -async fn buf_reader_to_lines( - reader: impl AsyncBufRead + Unpin, - limit: impl Into>, -) -> Result, Error> { - let mut lines = reader.lines(); - let mut answer = RingVec::new(limit.into().unwrap_or(1000)); - while let Some(line) = lines.next_line().await? { - answer.push(line); - } - let output: Vec = answer.value.into_iter().collect(); - Ok(output) -} - -enum MaxByLines { - Done(String), - Overflow(String), - Error(Error), -} - -async fn max_by_lines( - reader: impl AsyncBufRead + Unpin, - max_items: impl Into>, -) -> MaxByLines { - let mut answer = String::new(); - - let mut lines = reader.lines(); - let mut has_over_blown = false; - let max_items = max_items.into().unwrap_or(10_000_000); - - while let Some(line) = { - match lines.next_line().await { - Ok(a) => a, - Err(e) => return MaxByLines::Error(e.into()), - } - } { - if has_over_blown { - continue; - } - if !answer.is_empty() { - answer.push('\n'); - } - answer.push_str(&line); - if answer.len() >= max_items { - has_over_blown = true; - tracing::warn!("Reading the buffer exceeding limits of {}", max_items); - } - } - if has_over_blown { - return MaxByLines::Overflow(answer); - } - MaxByLines::Done(answer) -} - -#[cfg(test)] -mod tests { - use super::*; - /// Note, this size doesn't mean the vec will match. The vec will go to the next size, 0 -> 7 = 7 and so forth 7-15 = 15 - /// Just how the vec with capacity works. - const CAPACITY_IN: usize = 7; - #[test] - fn default_capacity_is_set() { - let ring: RingVec = RingVec::new(CAPACITY_IN); - assert_eq!(CAPACITY_IN, ring.value.capacity()); - assert_eq!(0, ring.value.len()); - } - #[test] - fn capacity_can_not_be_exceeded() { - let mut ring = RingVec::new(CAPACITY_IN); - for i in 1..100usize { - ring.push(i); - } - assert_eq!(CAPACITY_IN, ring.value.capacity()); - assert_eq!(CAPACITY_IN, ring.value.len()); - } - - #[test] - fn tests_buf_reader_to_lines() { - let mut reader = BufReader::new("hello\nworld\n".as_bytes()); - let lines = futures::executor::block_on(buf_reader_to_lines(&mut reader, None)).unwrap(); - assert_eq!(lines, vec!["hello", "world"]); - } -} diff --git a/core/startos/src/procedure/js_scripts.rs b/core/startos/src/procedure/js_scripts.rs deleted file mode 100644 index 43553cee0..000000000 --- a/core/startos/src/procedure/js_scripts.rs +++ /dev/null @@ -1,806 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Duration; - -use container_init::ProcessGroupId; -use helpers::UnixRpcClient; -pub use js_engine::JsError; -use js_engine::{JsExecutionEnvironment, PathForVolumeId}; -use models::VolumeId; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use tokio::process::Command; -use tracing::instrument; - -use super::ProcedureName; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::IoFormat; -use crate::util::{Invoke, Version}; -use crate::volume::Volumes; - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "kebab-case")] - -enum ErrorValue { - Error(String), - ErrorCode((i32, String)), - Result(serde_json::Value), -} - -impl PathForVolumeId for Volumes { - fn path_for( - &self, - data_dir: &Path, - package_id: &PackageId, - version: &Version, - volume_id: &VolumeId, - ) -> Option { - let volume = self.get(volume_id)?; - Some(volume.path_for(data_dir, package_id, version, volume_id)) - } - - fn readonly(&self, volume_id: &VolumeId) -> bool { - self.get(volume_id).map(|x| x.readonly()).unwrap_or(false) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ExecuteArgs { - pub procedure: JsProcedure, - pub directory: PathBuf, - pub pkg_id: PackageId, - pub pkg_version: Version, - pub name: ProcedureName, - pub volumes: Volumes, - pub input: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct JsProcedure { - #[serde(default)] - args: Vec, -} - -impl JsProcedure { - pub fn validate(&self, _volumes: &Volumes) -> Result<(), color_eyre::eyre::Report> { - Ok(()) - } - - #[instrument(skip_all)] - pub async fn execute( - &self, - directory: &PathBuf, - pkg_id: &PackageId, - pkg_version: &Version, - name: ProcedureName, - volumes: &Volumes, - input: Option, - timeout: Option, - _gid: ProcessGroupId, - _rpc_client: Option>, - ) -> Result, Error> { - #[cfg(not(test))] - let mut cmd = Command::new("start-deno"); - #[cfg(test)] - let mut cmd = test_start_deno_command().await?; - - cmd.arg("execute") - .input(Some(&mut std::io::Cursor::new(IoFormat::Json.to_vec( - &ExecuteArgs { - procedure: self.clone(), - directory: directory.clone(), - pkg_id: pkg_id.clone(), - pkg_version: pkg_version.clone(), - name, - volumes: volumes.clone(), - input: input.and_then(|x| serde_json::to_value(x).ok()), - }, - )?))) - .timeout(timeout) - .invoke(ErrorKind::Javascript) - .await - .and_then(|res| IoFormat::Json.from_slice(&res)) - } - - #[instrument(skip_all)] - pub async fn sandboxed( - &self, - directory: &PathBuf, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - input: Option, - timeout: Option, - name: ProcedureName, - ) -> Result, Error> { - #[cfg(not(test))] - let mut cmd = Command::new("start-deno"); - #[cfg(test)] - let mut cmd = test_start_deno_command().await?; - - cmd.arg("sandbox") - .input(Some(&mut std::io::Cursor::new(IoFormat::Json.to_vec( - &ExecuteArgs { - procedure: self.clone(), - directory: directory.clone(), - pkg_id: pkg_id.clone(), - pkg_version: pkg_version.clone(), - name, - volumes: volumes.clone(), - input: input.and_then(|x| serde_json::to_value(x).ok()), - }, - )?))) - .timeout(timeout) - .invoke(ErrorKind::Javascript) - .await - .and_then(|res| IoFormat::Json.from_slice(&res)) - } - - #[instrument(skip_all)] - pub async fn execute_impl( - &self, - directory: &PathBuf, - pkg_id: &PackageId, - pkg_version: &Version, - name: ProcedureName, - volumes: &Volumes, - input: Option, - ) -> Result, Error> { - let res = async move { - let running_action = JsExecutionEnvironment::load_from_package( - directory, - pkg_id, - pkg_version, - Box::new(volumes.clone()), - ) - .await? - .run_action(name, input, self.args.clone()); - let output: Option = running_action.await?; - let output: O = unwrap_known_error(output)?; - Ok(output) - } - .await - .map_err(|(error, message)| (error.as_code_num(), message)); - - Ok(res) - } - - #[instrument(skip_all)] - pub async fn sandboxed_impl( - &self, - directory: &PathBuf, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - input: Option, - name: ProcedureName, - ) -> Result, Error> { - Ok(async move { - let running_action = JsExecutionEnvironment::load_from_package( - directory, - pkg_id, - pkg_version, - Box::new(volumes.clone()), - ) - .await? - .read_only_effects() - .run_action(name, input, self.args.clone()); - let output: Option = running_action.await?; - let output: O = unwrap_known_error(output)?; - Ok(output) - } - .await - .map_err(|(error, message)| (error.as_code_num(), message))) - } -} - -fn unwrap_known_error( - error_value: Option, -) -> Result { - let error_value = error_value.unwrap_or_else(|| ErrorValue::Result(serde_json::Value::Null)); - match error_value { - ErrorValue::Error(error) => Err((JsError::Javascript, error)), - ErrorValue::ErrorCode((code, message)) => Err((JsError::Code(code), message)), - ErrorValue::Result(ref value) => match serde_json::from_value(value.clone()) { - Ok(a) => Ok(a), - Err(err) => { - tracing::error!("{}", err); - tracing::debug!("{:?}", err); - Err(( - JsError::BoundryLayerSerDe, - format!( - "Couldn't convert output = {:#?} to the correct type", - serde_json::to_string_pretty(&error_value).unwrap_or_default() - ), - )) - } - }, - } -} - -#[cfg(test)] -async fn test_start_deno_command() -> Result { - Command::new("cargo") - .arg("build") - .invoke(ErrorKind::Unknown) - .await?; - if tokio::fs::metadata("../target/debug/start-deno") - .await - .is_err() - { - Command::new("ln") - .arg("-rsf") - .arg("../target/debug/startbox") - .arg("../target/debug/start-deno") - .invoke(crate::ErrorKind::Filesystem) - .await?; - } - Ok(Command::new("../target/debug/start-deno")) -} - -#[tokio::test] -async fn js_action_execute() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::GetConfig; - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = Some(serde_json::json!({"test":123})); - let timeout = Some(Duration::from_secs(10)); - let _output: crate::config::action::ConfigRes = js_action - .execute( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - &std::fs::read_to_string( - "test/js_action_execute/package-data/volumes/test-package/data/main/test.log" - ) - .unwrap(), - "This is a test" - ); - std::fs::remove_file( - "test/js_action_execute/package-data/volumes/test-package/data/main/test.log", - ) - .unwrap(); -} - -#[tokio::test] -async fn js_action_execute_error() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::SetConfig; - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - let output: Result = js_action - .execute( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap(); - assert_eq!("Err((2, \"Not setup\"))", &format!("{:?}", output)); -} - -#[tokio::test] -async fn js_action_fetch() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("fetch".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} - -#[tokio::test] -async fn js_test_slow() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("slow".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - tracing::debug!("testing start"); - tokio::select! { - a = js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) => { a.unwrap().unwrap(); }, - _ = tokio::time::sleep(Duration::from_secs(1)) => () - } - tracing::debug!("testing end should"); - tokio::time::sleep(Duration::from_secs(2)).await; - tracing::debug!("Done"); -} -#[tokio::test] -async fn js_action_var_arg() { - let js_action = JsProcedure { - args: vec![42.into()], - }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("js-action-var-arg".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} - -#[tokio::test] -async fn js_action_test_rename() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-rename".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} - -#[tokio::test] -async fn js_action_test_deep_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-deep-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} -#[tokio::test] -async fn js_action_test_deep_dir_escape() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} -#[tokio::test] -async fn js_action_test_zero_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-zero-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} -#[tokio::test] -async fn js_action_test_read_dir() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-read-dir".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} - -#[tokio::test] -async fn js_rsync() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-rsync".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} - -#[tokio::test] -async fn js_disk_usage() { - let js_action = JsProcedure { args: vec![] }; - let path: PathBuf = "test/js_action_execute/" - .parse::() - .unwrap() - .canonicalize() - .unwrap(); - let package_id = "test-package".parse().unwrap(); - let package_version: Version = "0.3.0.3".parse().unwrap(); - let name = ProcedureName::Action("test-disk-usage".parse().unwrap()); - let volumes: Volumes = serde_json::from_value(serde_json::json!({ - "main": { - "type": "data" - }, - "compat": { - "type": "assets" - }, - "filebrowser" :{ - "package-id": "filebrowser", - "path": "data", - "readonly": true, - "type": "pointer", - "volume-id": "main", - } - })) - .unwrap(); - let input: Option = None; - let timeout = Some(Duration::from_secs(10)); - js_action - .execute::( - &path, - &package_id, - &package_version, - name, - &volumes, - input, - timeout, - ProcessGroupId(0), - None, - ) - .await - .unwrap() - .unwrap(); -} diff --git a/core/startos/src/procedure/mod.rs b/core/startos/src/procedure/mod.rs deleted file mode 100644 index f3a528713..000000000 --- a/core/startos/src/procedure/mod.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::collections::BTreeSet; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use models::ImageId; -use patch_db::HasModel; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use self::docker::DockerProcedure; -use crate::context::RpcContext; -use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::Version; -use crate::volume::Volumes; -use crate::{Error, ErrorKind}; - -pub mod docker; -#[cfg(feature = "js-engine")] -pub mod js_scripts; -pub use models::ProcedureName; - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "type")] -#[model = "Model"] -pub enum PackageProcedure { - Docker(DockerProcedure), - - #[cfg(feature = "js-engine")] - Script(js_scripts::JsProcedure), -} - -impl PackageProcedure { - pub fn is_script(&self) -> bool { - match self { - #[cfg(feature = "js-engine")] - Self::Script(_) => true, - _ => false, - } - } - #[instrument(skip_all)] - pub fn validate( - &self, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - expected_io: bool, - ) -> Result<(), color_eyre::eyre::Report> { - match self { - PackageProcedure::Docker(action) => { - action.validate(eos_version, volumes, image_ids, expected_io) - } - #[cfg(feature = "js-engine")] - PackageProcedure::Script(action) => action.validate(volumes), - } - } - - #[instrument(skip_all)] - pub async fn execute( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - name: ProcedureName, - volumes: &Volumes, - input: Option, - timeout: Option, - ) -> Result, Error> { - tracing::trace!("Procedure execute {} {} - {:?}", self, pkg_id, name); - match self { - PackageProcedure::Docker(procedure) if procedure.inject == true => { - procedure - .inject(ctx, pkg_id, pkg_version, name, volumes, input, timeout) - .await - } - PackageProcedure::Docker(procedure) => { - procedure - .execute(ctx, pkg_id, pkg_version, name, volumes, input, timeout) - .await - } - #[cfg(feature = "js-engine")] - PackageProcedure::Script(procedure) => { - let man = ctx - .managers - .get(&(pkg_id.clone(), pkg_version.clone())) - .await - .ok_or_else(|| { - Error::new( - eyre!("No manager found for {}", pkg_id), - ErrorKind::NotFound, - ) - })?; - let rpc_client = man.rpc_client(); - let gid = if matches!(name, ProcedureName::Main) { - man.gid.new_main_gid() - } else { - man.gid.new_gid() - }; - - procedure - .execute( - &ctx.datadir, - pkg_id, - pkg_version, - name, - volumes, - input, - timeout, - gid, - rpc_client, - ) - .await - } - } - } - - #[instrument(skip_all)] - pub async fn sandboxed( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - input: Option, - timeout: Option, - name: ProcedureName, - ) -> Result, Error> { - tracing::trace!("Procedure sandboxed {} {} - {:?}", self, pkg_id, name); - match self { - PackageProcedure::Docker(procedure) => { - procedure - .sandboxed(ctx, pkg_id, pkg_version, volumes, input, timeout) - .await - } - #[cfg(feature = "js-engine")] - PackageProcedure::Script(procedure) => { - procedure - .sandboxed( - &ctx.datadir, - pkg_id, - pkg_version, - volumes, - input, - timeout, - name, - ) - .await - } - } - } -} - -impl std::fmt::Display for PackageProcedure { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PackageProcedure::Docker(_) => write!(f, "Docker")?, - #[cfg(feature = "js-engine")] - PackageProcedure::Script(_) => write!(f, "JS")?, - } - Ok(()) - } -} - -// TODO: make this not allocate -#[derive(Debug)] -pub struct NoOutput; -impl<'de> Deserialize<'de> for NoOutput { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let _ = Value::deserialize(deserializer); - Ok(NoOutput) - } -} - -#[test] -fn test_deser_no_output() { - serde_json::from_str::("").unwrap(); - serde_json::from_str::>("{\"Ok\": null}") - .unwrap() - .unwrap(); -} diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs new file mode 100644 index 000000000..cc3257132 --- /dev/null +++ b/core/startos/src/progress.rs @@ -0,0 +1,516 @@ +use std::panic::UnwindSafe; +use std::time::Duration; + +use futures::future::pending; +use futures::stream::BoxStream; +use futures::{Future, FutureExt, StreamExt, TryFutureExt}; +use helpers::NonDetachingJoinHandle; +use imbl_value::{InOMap, InternedString}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncSeek, AsyncWrite}; +use tokio::sync::watch; +use ts_rs::TS; + +use crate::db::model::{Database, DatabaseModel}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref SPINNER: ProgressStyle = ProgressStyle::with_template("{spinner} {msg}...").unwrap(); + static ref PERCENTAGE: ProgressStyle = ProgressStyle::with_template("{msg} {percent}% {wide_bar} [{human_pos}/{human_len}] [{per_sec} {eta}]").unwrap(); + static ref PERCENTAGE_BYTES: ProgressStyle = ProgressStyle::with_template("{msg} {percent}% {wide_bar} [{binary_bytes}/{binary_total_bytes}] [{binary_bytes_per_sec} {eta}]").unwrap(); + static ref STEPS: ProgressStyle = ProgressStyle::with_template("{spinner} {wide_msg} [{human_pos}/?] [{per_sec} {elapsed}]").unwrap(); + static ref BYTES: ProgressStyle = ProgressStyle::with_template("{spinner} {wide_msg} [{bytes}/?] [{binary_bytes_per_sec} {elapsed}]").unwrap(); +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] +#[serde(untagged)] +pub enum Progress { + NotStarted(()), + Complete(bool), + Progress { + #[ts(type = "number")] + done: u64, + #[ts(type = "number | null")] + total: Option, + }, +} +impl Progress { + pub fn new() -> Self { + Progress::NotStarted(()) + } + pub fn update_bar(self, bar: &ProgressBar, bytes: bool) { + match self { + Self::NotStarted(()) => { + bar.set_style(SPINNER.clone()); + } + Self::Complete(false) => { + bar.set_style(SPINNER.clone()); + bar.tick(); + } + Self::Complete(true) => { + bar.finish(); + } + Self::Progress { done, total: None } => { + if bytes { + bar.set_style(BYTES.clone()); + } else { + bar.set_style(STEPS.clone()); + } + bar.set_position(done); + bar.tick(); + } + Self::Progress { + done, + total: Some(total), + } => { + if bytes { + bar.set_style(PERCENTAGE_BYTES.clone()); + } else { + bar.set_style(PERCENTAGE.clone()); + } + bar.set_position(done); + bar.set_length(total); + bar.tick(); + } + } + } + pub fn start(&mut self) { + *self = match *self { + Self::NotStarted(()) => Self::Complete(false), + a => a, + }; + } + pub fn set_done(&mut self, done: u64) { + *self = match *self { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done, total: None }, + Self::Progress { mut done, total } => { + if let Some(total) = total { + if done > total { + done = total; + } + } + Self::Progress { done, total } + } + Self::Complete(true) => Self::Complete(true), + }; + } + pub fn set_total(&mut self, total: u64) { + *self = match *self { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { + done: 0, + total: Some(total), + }, + Self::Progress { done, .. } => Self::Progress { + done, + total: Some(total), + }, + Self::Complete(true) => Self::Complete(true), + } + } + pub fn add_total(&mut self, total: u64) { + if let Self::Progress { + done, + total: Some(old), + } = *self + { + *self = Self::Progress { + done, + total: Some(old + total), + }; + } else { + self.set_total(total) + } + } + pub fn complete(&mut self) { + *self = Self::Complete(true); + } + pub fn is_complete(&self) -> bool { + matches!(self, Self::Complete(true)) + } +} +impl std::ops::Add for Progress { + type Output = Self; + fn add(self, rhs: u64) -> Self::Output { + match self { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { + done: rhs, + total: None, + }, + Self::Progress { done, total } => { + let mut done = done + rhs; + if let Some(total) = total { + if done > total { + done = total; + } + } + Self::Progress { done, total } + } + Self::Complete(true) => Self::Complete(true), + } + } +} +impl std::ops::AddAssign for Progress { + fn add_assign(&mut self, rhs: u64) { + *self = *self + rhs; + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct NamedProgress { + #[ts(type = "string")] + pub name: InternedString, + pub progress: Progress, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct FullProgress { + pub overall: Progress, + pub phases: Vec, +} +impl FullProgress { + pub fn new() -> Self { + Self { + overall: Progress::new(), + phases: Vec::new(), + } + } +} + +#[derive(Clone)] +pub struct FullProgressTracker { + overall: watch::Sender, + phases: watch::Sender>>, +} +impl FullProgressTracker { + pub fn new() -> Self { + let (overall, _) = watch::channel(Progress::new()); + let (phases, _) = watch::channel(InOMap::new()); + Self { overall, phases } + } + pub fn snapshot(&self) -> FullProgress { + FullProgress { + overall: *self.overall.borrow(), + phases: self + .phases + .borrow() + .iter() + .map(|(name, progress)| NamedProgress { + name: name.clone(), + progress: *progress.borrow(), + }) + .collect(), + } + } + pub fn stream(&self, min_interval: Option) -> BoxStream<'static, FullProgress> { + struct StreamState { + overall: watch::Receiver, + phases_recv: watch::Receiver>>, + phases: InOMap>, + } + let mut overall = self.overall.subscribe(); + overall.mark_changed(); // make sure stream starts with a value + let phases_recv = self.phases.subscribe(); + let phases = phases_recv.borrow().clone(); + let state = StreamState { + overall, + phases_recv, + phases, + }; + futures::stream::unfold( + state, + move |StreamState { + mut overall, + mut phases_recv, + mut phases, + }| async move { + let changed = phases + .iter_mut() + .map(|(_, p)| async move { p.changed().or_else(|_| pending()).await }.boxed()) + .chain([overall.changed().boxed()]) + .chain([phases_recv.changed().boxed()]) + .map(|fut| fut.map(|r| r.unwrap_or_default())) + .collect_vec(); + if let Some(min_interval) = min_interval { + tokio::join!( + tokio::time::sleep(min_interval), + futures::future::select_all(changed), + ); + } else { + futures::future::select_all(changed).await; + } + + for (name, phase) in &*phases_recv.borrow_and_update() { + if !phases.contains_key(name) { + phases.insert(name.clone(), phase.clone()); + } + } + + let o = *overall.borrow_and_update(); + + Some(( + FullProgress { + overall: o, + phases: phases + .iter_mut() + .map(|(name, progress)| NamedProgress { + name: name.clone(), + progress: *progress.borrow_and_update(), + }) + .collect(), + }, + StreamState { + overall, + phases_recv, + phases, + }, + )) + }, + ) + .boxed() + } + pub fn sync_to_db( + &self, + db: TypedPatchDb, + deref: DerefFn, + min_interval: Option, + ) -> impl Future> + 'static + where + DerefFn: Fn(&mut DatabaseModel) -> Option<&mut Model> + 'static, + for<'a> &'a DerefFn: UnwindSafe + Send, + { + let mut stream = self.stream(min_interval); + async move { + while let Some(progress) = stream.next().await { + if db + .mutate(|v| { + if let Some(p) = deref(v) { + p.ser(&progress)?; + Ok(progress.overall.is_complete()) + } else { + Ok(true) + } + }) + .await? + { + break; + } + } + Ok(()) + } + } + pub fn progress_bar_task(&self, name: &str) -> NonDetachingJoinHandle<()> { + let mut stream = self.stream(None); + let mut bar = PhasedProgressBar::new(name); + tokio::spawn(async move { + while let Some(progress) = stream.next().await { + bar.update(&progress); + if progress.overall.is_complete() { + break; + } + } + }) + .into() + } + pub fn add_phase( + &self, + name: InternedString, + overall_contribution: Option, + ) -> PhaseProgressTrackerHandle { + if let Some(overall_contribution) = overall_contribution { + self.overall + .send_modify(|o| o.add_total(overall_contribution)); + } + let (send, recv) = watch::channel(Progress::new()); + self.phases.send_modify(|p| { + p.insert(name, recv); + }); + PhaseProgressTrackerHandle { + overall: self.overall.clone(), + overall_contribution, + contributed: 0, + progress: send, + } + } + pub fn complete(&self) { + self.overall.send_modify(|o| o.complete()); + } +} + +pub struct PhaseProgressTrackerHandle { + overall: watch::Sender, + overall_contribution: Option, + contributed: u64, + progress: watch::Sender, +} +impl PhaseProgressTrackerHandle { + fn update_overall(&mut self) { + if let Some(overall_contribution) = self.overall_contribution { + let contribution = match *self.progress.borrow() { + Progress::Complete(true) => overall_contribution, + Progress::Progress { + done, + total: Some(total), + } => ((done as f64 / total as f64) * overall_contribution as f64) as u64, + _ => 0, + }; + if contribution > self.contributed { + self.overall + .send_modify(|o| *o += contribution - self.contributed); + self.contributed = contribution; + } + } + } + pub fn start(&mut self) { + self.progress.send_modify(|p| p.start()); + } + pub fn set_done(&mut self, done: u64) { + self.progress.send_modify(|p| p.set_done(done)); + self.update_overall(); + } + pub fn set_total(&mut self, total: u64) { + self.progress.send_modify(|p| p.set_total(total)); + self.update_overall(); + } + pub fn add_total(&mut self, total: u64) { + self.progress.send_modify(|p| p.add_total(total)); + self.update_overall(); + } + pub fn complete(&mut self) { + self.progress.send_modify(|p| p.complete()); + self.update_overall(); + } + pub fn writer(self, writer: W) -> ProgressTrackerWriter { + ProgressTrackerWriter { + writer, + progress: self, + } + } +} +impl std::ops::AddAssign for PhaseProgressTrackerHandle { + fn add_assign(&mut self, rhs: u64) { + self.progress.send_modify(|p| *p += rhs); + self.update_overall(); + } +} + +#[pin_project::pin_project] +pub struct ProgressTrackerWriter { + #[pin] + writer: W, + progress: PhaseProgressTrackerHandle, +} +impl ProgressTrackerWriter { + pub fn new(writer: W, progress: PhaseProgressTrackerHandle) -> Self { + Self { writer, progress } + } + pub fn into_inner(self) -> (W, PhaseProgressTrackerHandle) { + (self.writer, self.progress) + } +} +impl AsyncWrite for ProgressTrackerWriter { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let this = self.project(); + match this.writer.poll_write(cx, buf) { + std::task::Poll::Ready(Ok(n)) => { + *this.progress += n as u64; + std::task::Poll::Ready(Ok(n)) + } + a => a, + } + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().writer.poll_flush(cx) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().writer.poll_shutdown(cx) + } + fn is_write_vectored(&self) -> bool { + self.writer.is_write_vectored() + } + fn poll_write_vectored( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> std::task::Poll> { + self.project().writer.poll_write_vectored(cx, bufs) + } +} +impl AsyncSeek for ProgressTrackerWriter { + fn start_seek( + self: std::pin::Pin<&mut Self>, + position: std::io::SeekFrom, + ) -> std::io::Result<()> { + self.project().writer.start_seek(position) + } + fn poll_complete( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + match this.writer.poll_complete(cx) { + std::task::Poll::Ready(Ok(n)) => { + this.progress.set_done(n); + std::task::Poll::Ready(Ok(n)) + } + a => a, + } + } +} + +pub struct PhasedProgressBar { + multi: MultiProgress, + overall: ProgressBar, + phases: InOMap, +} +impl PhasedProgressBar { + pub fn new(name: &str) -> Self { + let multi = MultiProgress::new(); + Self { + overall: multi.add( + ProgressBar::new(0) + .with_style(SPINNER.clone()) + .with_message(name.to_owned()), + ), + multi, + phases: InOMap::new(), + } + } + pub fn update(&mut self, progress: &FullProgress) { + for phase in progress.phases.iter() { + if !self.phases.contains_key(&phase.name) { + self.phases.insert( + phase.name.clone(), + self.multi + .add(ProgressBar::new(0).with_style(SPINNER.clone())) + .with_message((&*phase.name).to_owned()), + ); + } + } + progress.overall.update_bar(&self.overall, false); + for (name, bar) in self.phases.iter() { + if let Some(progress) = progress.phases.iter().find_map(|p| { + if &p.name == name { + Some(p.progress) + } else { + None + } + }) { + progress.update_bar(bar, true); + } + } + } +} diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs deleted file mode 100644 index 851033b71..000000000 --- a/core/startos/src/properties.rs +++ /dev/null @@ -1,50 +0,0 @@ -use clap::ArgMatches; -use color_eyre::eyre::eyre; -use rpc_toolkit::command; -use serde_json::Value; -use tracing::instrument; - -use crate::context::RpcContext; -use crate::prelude::*; -use crate::procedure::ProcedureName; -use crate::s9pk::manifest::PackageId; -use crate::{Error, ErrorKind}; - -pub fn display_properties(response: Value, _: &ArgMatches) { - println!("{}", response); -} - -#[command(display(display_properties))] -pub async fn properties(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result { - Ok(fetch_properties(ctx, id).await?) -} - -#[instrument(skip_all)] -pub async fn fetch_properties(ctx: RpcContext, id: PackageId) -> Result { - let peek = ctx.db.peek().await; - - let manifest = peek - .as_package_data() - .as_idx(&id) - .ok_or_else(|| Error::new(eyre!("{} is not installed", id), ErrorKind::NotFound))? - .expect_as_installed()? - .as_manifest() - .de()?; - if let Some(props) = manifest.properties { - props - .execute::<(), Value>( - &ctx, - &manifest.id, - &manifest.version, - ProcedureName::Properties, - &manifest.volumes, - None, - None, - ) - .await? - .map_err(|(_, e)| Error::new(eyre!("{}", e), ErrorKind::Docker)) - .and_then(|a| Ok(a)) - } else { - Ok(Value::Null) - } -} diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index e994b5c53..f3cac9f7e 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -1,214 +1,334 @@ +use std::collections::BTreeMap; use std::path::PathBuf; -use std::time::Duration; - -use color_eyre::eyre::eyre; -use console::style; -use futures::StreamExt; -use indicatif::{ProgressBar, ProgressStyle}; -use reqwest::{header, Body, Client, Url}; -use rpc_toolkit::command; - -use crate::s9pk::reader::S9pkReader; -use crate::util::display_none; -use crate::{Error, ErrorKind}; - -async fn registry_user_pass(location: &str) -> Result<(Url, String, String), Error> { - let mut url = Url::parse(location)?; - let user = url.username().to_string(); - let pass = url.password().map(str::to_string); - if user.is_empty() || url.path() != "/" { - return Err(Error::new( - eyre!("{location:?} is not like \"https://user@registry.example.com/\""), - ErrorKind::ParseUrl, - )); + +use clap::Parser; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::signer::sign::AnyVerifyingKey; +use crate::registry::signer::{ContactInfo, SignerInfo}; +use crate::registry::RegistryDatabase; +use crate::rpc_continuations::Guid; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; + +pub fn admin_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "signer", + signers_api::().with_about("Commands to add or list signers"), + ) + .subcommand("add", from_fn_async(add_admin).no_cli()) + .subcommand( + "add", + from_fn_async(cli_add_admin) + .no_display() + .with_about("Add admin signer"), + ) + .subcommand( + "list", + from_fn_async(list_admins) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_about("List admin signers") + .with_call_remote::(), + ) +} + +fn signers_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list_signers) + .with_metadata("admin", Value::Bool(true)) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_about("List signers") + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add_signer) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "add", + from_fn_async(cli_add_signer).with_about("Add signer"), + ) + .subcommand( + "edit", + from_fn_async(edit_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_call_remote::(), + ) +} + +impl Model> { + pub fn get_signer(&self, key: &AnyVerifyingKey) -> Result { + self.as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.as_keys().de()?))) + .filter_ok(|(_, s)| s.contains(key)) + .next() + .transpose()? + .map(|(a, _)| a) + .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) + } + + pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> { + self.as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.de()?))) + .filter_ok(|(_, s)| s.keys.contains(key)) + .next() + .transpose()? + .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - let _ = url.set_username(""); - let _ = url.set_password(None); - - let pass = match pass { - Some(p) => p, - None => { - let pass_prompt = format!("{} Password for {user}: ", style("?").yellow()); - tokio::task::spawn_blocking(move || rpassword::prompt_password(pass_prompt)) - .await - .unwrap()? + + pub fn add_signer(&mut self, signer: &SignerInfo) -> Result { + if let Some((guid, s)) = self + .as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.de()?))) + .filter_ok(|(_, s)| !s.keys.is_disjoint(&signer.keys)) + .next() + .transpose()? + { + return Err(Error::new( + eyre!( + "A signer {} ({}) already exists with a matching key", + guid, + s.name + ), + ErrorKind::InvalidRequest, + )); } - }; - Ok((url, user.to_string(), pass.to_string())) + let id = Guid::new(); + self.insert(&id, signer)?; + Ok(id) + } } -#[derive(serde::Serialize, Debug)] -struct Package { - id: String, - version: String, - arches: Option>, +pub async fn list_signers(ctx: RegistryContext) -> Result, Error> { + ctx.db.peek().await.into_index().into_signers().de() } -async fn do_index( - httpc: &Client, - mut url: Url, - user: &str, - pass: &str, - pkg: &Package, -) -> Result<(), Error> { - url.set_path("/admin/v0/index"); - let req = httpc - .post(url) - .header(header::ACCEPT, "text/plain") - .basic_auth(user, Some(pass)) - .json(pkg) - .build()?; - let res = httpc.execute(req).await?; - if !res.status().is_success() { - let info = res.text().await?; - return Err(Error::new(eyre!("{}", info), ErrorKind::Registry)); +pub fn display_signers(params: WithIoFormat, signers: BTreeMap) { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, signers); } - Ok(()) -} -async fn do_upload( - httpc: &Client, - mut url: Url, - user: &str, - pass: &str, - pkg_id: &str, - body: Body, -) -> Result<(), Error> { - url.set_path("/admin/v0/upload"); - let req = httpc - .post(url) - .header(header::ACCEPT, "text/plain") - .query(&[("id", pkg_id)]) - .basic_auth(user, Some(pass)) - .body(body) - .build()?; - let res = httpc.execute(req).await?; - if !res.status().is_success() { - let info = res.text().await?; - return Err(Error::new(eyre!("{}", info), ErrorKind::Registry)); + let mut table = Table::new(); + table.add_row(row![bc => + "ID", + "NAME", + "CONTACT", + "KEYS", + ]); + for (id, info) in signers { + table.add_row(row![ + id.as_ref(), + &info.name, + &info.contact.into_iter().join("\n"), + &info.keys.into_iter().join("\n"), + ]); } - Ok(()) + table.print_tty(false).unwrap(); } -#[command(cli_only, display(display_none))] -pub async fn publish( - #[arg] location: String, - #[arg] path: PathBuf, - #[arg(rename = "no-verify", long = "no-verify")] no_verify: bool, - #[arg(rename = "no-upload", long = "no-upload")] no_upload: bool, - #[arg(rename = "no-index", long = "no-index")] no_index: bool, +pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result { + ctx.db + .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[ts(export)] +pub struct EditSignerParams { + pub id: Guid, + #[arg(short = 'n', long)] + pub set_name: Option, + #[arg(short = 'c', long)] + pub add_contact: Vec, + #[arg(short = 'k', long)] + pub add_key: Vec, + #[arg(short = 'C', long)] + pub remove_contact: Vec, + #[arg(short = 'K', long)] + pub remove_key: Vec, +} + +pub async fn edit_signer( + ctx: RegistryContext, + EditSignerParams { + id, + set_name, + add_contact, + add_key, + remove_contact, + remove_key, + }: EditSignerParams, ) -> Result<(), Error> { - // Prepare for progress bars. - let bytes_bar_style = - ProgressStyle::with_template("{percent}% {wide_bar} [{bytes}/{total_bytes}] [{eta}]") - .unwrap(); - let plain_line_style = - ProgressStyle::with_template("{prefix:.bold.dim} {wide_msg}...").unwrap(); - let spinner_line_style = - ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}...").unwrap(); - - // Read the file to get manifest information and check validity.. - // Open file right away so it can not change out from under us. - let file = tokio::fs::File::open(&path).await?; - - let manifest = if no_verify { - let pb = ProgressBar::new(1) - .with_style(spinner_line_style.clone()) - .with_prefix("[1/3]") - .with_message("Querying s9pk"); - pb.enable_steady_tick(Duration::from_millis(200)); - let mut s9pk = S9pkReader::open(&path, false).await?; - let m = s9pk.manifest().await?.clone(); - pb.set_style(plain_line_style.clone()); - pb.abandon(); - m - } else { - let pb = ProgressBar::new(1) - .with_style(spinner_line_style.clone()) - .with_prefix("[1/3]") - .with_message("Verifying s9pk"); - pb.enable_steady_tick(Duration::from_millis(200)); - let mut s9pk = S9pkReader::open(&path, true).await?; - s9pk.validate().await?; - let m = s9pk.manifest().await?.clone(); - pb.set_style(plain_line_style.clone()); - pb.abandon(); - m - }; - let pkg = Package { - id: manifest.id.to_string(), - version: manifest.version.to_string(), - arches: manifest.hardware_requirements.arch.clone(), + ctx.db + .mutate(|db| { + db.as_index_mut() + .as_signers_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .mutate(|s| { + if let Some(name) = set_name { + s.name = name; + } + s.contact.extend(add_contact); + for rm in remove_contact { + let Some((idx, _)) = s.contact.iter().enumerate().find(|(_, c)| *c == &rm) + else { + continue; + }; + s.contact.remove(idx); + } + + s.keys.extend(add_key); + for rm in remove_key { + s.keys.remove(&rm); + } + Ok(()) + }) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddSignerParams { + #[arg(long = "name", short = 'n')] + pub name: String, + #[arg(long = "contact", short = 'c')] + pub contact: Vec, + #[arg(long = "key")] + pub keys: Vec, + pub database: Option, +} + +pub async fn cli_add_signer( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliAddSignerParams { + name, + contact, + keys, + database, + }, + .. + }: HandlerArgs, +) -> Result { + let signer = SignerInfo { + name, + contact, + keys: keys.into_iter().collect(), }; - println!("{} id = {}", style(">").green(), pkg.id); - println!("{} version = {}", style(">").green(), pkg.version); - if let Some(arches) = &pkg.arches { - println!("{} arches = {:?}", style(">").green(), arches); + if let Some(database) = database { + TypedPatchDb::::load(PatchDb::open(database).await?) + .await? + .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) + .await } else { - println!( - "{} No architecture listed in hardware_requirements", - style(">").red() - ); + from_value( + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&signer)?, + ) + .await?, + ) } +} - // Process the url and get the user's password. - let (registry, user, pass) = registry_user_pass(&location).await?; - - // Now prepare a stream of the file which will show a progress bar as it is consumed. - let file_size = file.metadata().await?.len(); - let file_stream = tokio_util::io::ReaderStream::new(file); - ProgressBar::new(0) - .with_style(plain_line_style.clone()) - .with_prefix("[2/3]") - .with_message("Uploading s9pk") - .abandon(); - let pb = ProgressBar::new(file_size).with_style(bytes_bar_style.clone()); - let stream_pb = pb.clone(); - let file_stream = file_stream.inspect(move |bytes| { - if let Ok(bytes) = bytes { - stream_pb.inc(bytes.len() as u64); - } - }); +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddAdminParams { + pub signer: Guid, +} + +pub async fn add_admin( + ctx: RegistryContext, + AddAdminParams { signer }: AddAdminParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?; + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddAdminParams { + pub signer: Guid, + pub database: Option, +} - let httpc = Client::builder().build().unwrap(); - // And upload! - if no_upload { - println!("{} Skipping upload", style(">").yellow()); +pub async fn cli_add_admin( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliAddAdminParams { signer, database }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + if let Some(database) = database { + TypedPatchDb::::load(PatchDb::open(database).await?) + .await? + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?; + Ok(()) + }) + .await?; } else { - do_upload( - &httpc, - registry.clone(), - &user, - &pass, - &pkg.id, - Body::wrap_stream(file_stream), + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&AddAdminParams { signer })?, ) .await?; } - pb.finish_and_clear(); - - // Also index, so it will show up in the registry. - let pb = ProgressBar::new(0) - .with_style(spinner_line_style.clone()) - .with_prefix("[3/3]") - .with_message("Indexing registry"); - pb.enable_steady_tick(Duration::from_millis(200)); - if no_index { - println!("{} Skipping index", style(">").yellow()); - } else { - do_index(&httpc, registry.clone(), &user, &pass, &pkg).await?; - } - pb.set_style(plain_line_style.clone()); - pb.abandon(); - - // All done - if !no_index { - println!( - "{} Package {} is now published to {}", - style(">").green(), - pkg.id, - registry - ); - } Ok(()) } + +pub async fn list_admins(ctx: RegistryContext) -> Result, Error> { + let db = ctx.db.peek().await; + let admins = db.as_admins().de()?; + Ok(db + .into_index() + .into_signers() + .de()? + .into_iter() + .filter(|(id, _)| admins.contains(id)) + .collect()) +} diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs new file mode 100644 index 000000000..fb6dd59fc --- /dev/null +++ b/core/startos/src/registry/asset.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use helpers::NonDetachingJoinHandle; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::registry::signer::AcceptSigners; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; +use crate::s9pk::S9pk; +use crate::upload::UploadingFile; + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RegistryAsset { + #[ts(type = "string")] + pub published_at: DateTime, + #[ts(type = "string")] + pub url: Url, + pub commitment: Commitment, + pub signatures: HashMap, +} +impl RegistryAsset { + pub fn all_signers(&self) -> AcceptSigners { + AcceptSigners::All( + self.signatures + .keys() + .cloned() + .map(AcceptSigners::Signer) + .collect(), + ) + } +} +impl RegistryAsset { + pub fn validate(&self, context: &str, mut accept: AcceptSigners) -> Result<&Commitment, Error> { + for (signer, signature) in &self.signatures { + accept.process_signature(signer, &self.commitment, context, signature)?; + } + accept.try_accept()?; + Ok(&self.commitment) + } +} +impl Commitment<&'a HttpSource>> RegistryAsset { + pub async fn download( + &self, + client: Client, + dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), + ) -> Result<(), Error> { + self.commitment + .copy_to(&HttpSource::new(client, self.url.clone()).await?, dst) + .await + } +} +impl RegistryAsset { + pub async fn deserialize_s9pk( + &self, + client: Client, + ) -> Result>>, Error> { + S9pk::deserialize( + &Arc::new(HttpSource::new(client, self.url.clone()).await?), + Some(&self.commitment), + ) + .await + } + pub async fn deserialize_s9pk_buffered( + &self, + client: Client, + ) -> Result>>, Error> { + S9pk::deserialize( + &Arc::new(BufferedHttpSource::new(client, self.url.clone()).await?), + Some(&self.commitment), + ) + .await + } +} + +pub struct BufferedHttpSource { + _download: NonDetachingJoinHandle<()>, + file: UploadingFile, +} +impl BufferedHttpSource { + pub async fn new(client: Client, url: Url) -> Result { + let (mut handle, file) = UploadingFile::new().await?; + let response = client.get(url).send().await?; + Ok(Self { + _download: tokio::spawn(async move { handle.download(response).await }).into(), + file, + }) + } +} +impl ArchiveSource for BufferedHttpSource { + type FetchReader = ::FetchReader; + type FetchAllReader = ::FetchAllReader; + async fn size(&self) -> Option { + self.file.size().await + } + async fn fetch_all(&self) -> Result { + self.file.fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.file.fetch(position, size).await + } +} diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs new file mode 100644 index 000000000..4707bf809 --- /dev/null +++ b/core/startos/src/registry/auth.rs @@ -0,0 +1,222 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use axum::body::Body; +use axum::extract::Request; +use axum::response::Response; +use chrono::Utc; +use http::HeaderValue; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::signer::commitment::request::RequestCommitment; +use crate::registry::signer::commitment::Commitment; +use crate::registry::signer::sign::{ + AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme, +}; +use crate::util::serde::Base64; + +pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; + +#[derive(Deserialize)] +pub struct Metadata { + #[serde(default)] + admin: bool, + #[serde(default)] + get_signer: bool, +} + +#[derive(Clone)] +pub struct Auth { + nonce_cache: Arc>>, // for replay protection + signer: Option>, +} +impl Auth { + pub fn new() -> Self { + Self { + nonce_cache: Arc::new(Mutex::new(BTreeMap::new())), + signer: None, + } + } + async fn handle_nonce(&mut self, nonce: u64) -> Result<(), Error> { + let mut cache = self.nonce_cache.lock().await; + if cache.values().any(|n| *n == nonce) { + return Err(Error::new( + eyre!("replay attack detected"), + ErrorKind::Authorization, + )); + } + while let Some(entry) = cache.first_entry() { + if entry.key().elapsed() > Duration::from_secs(60) { + entry.remove_entry(); + } else { + break; + } + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize, TS)] +pub struct RegistryAdminLogRecord { + pub timestamp: String, + pub name: String, + #[ts(type = "{ id: string | number | null; method: string; params: any }")] + pub request: RpcRequest, + pub key: AnyVerifyingKey, +} + +pub struct SignatureHeader { + pub commitment: RequestCommitment, + pub signer: AnyVerifyingKey, + pub signature: AnySignature, +} +impl SignatureHeader { + pub fn to_header(&self) -> HeaderValue { + let mut url: Url = "http://localhost".parse().unwrap(); + self.commitment.append_query(&mut url); + url.query_pairs_mut() + .append_pair("signer", &self.signer.to_string()); + url.query_pairs_mut() + .append_pair("signature", &self.signature.to_string()); + HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() + } + pub fn from_header(header: &HeaderValue) -> Result { + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); + Ok(Self { + commitment: RequestCommitment::from_query(&header)?, + signer: query.get("signer").or_not_found("signer")?.parse()?, + signature: query.get("signature").or_not_found("signature")?.parse()?, + }) + } + pub fn sign(signer: &AnySigningKey, body: &[u8], context: &str) -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); + let nonce = rand::random(); + let commitment = RequestCommitment { + timestamp, + nonce, + size: body.len() as u64, + blake3: Base64(*blake3::hash(body).as_bytes()), + }; + let signature = signer + .scheme() + .sign_commitment(&signer, &commitment, context)?; + Ok(Self { + commitment, + signer: signer.verifying_key(), + signature, + }) + } +} + +impl Middleware for Auth { + type Metadata = Metadata; + async fn process_http_request( + &mut self, + ctx: &RegistryContext, + request: &mut Request, + ) -> Result<(), Response> { + if request.headers().contains_key(AUTH_SIG_HEADER) { + self.signer = Some( + async { + let SignatureHeader { + commitment, + signer, + signature, + } = SignatureHeader::from_header( + request + .headers() + .get(AUTH_SIG_HEADER) + .or_not_found("missing X-StartOS-Registry-Auth-Sig") + .with_kind(ErrorKind::InvalidRequest)?, + )?; + + signer.scheme().verify_commitment( + &signer, + &commitment, + &ctx.hostname, + &signature, + )?; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); + if (now - commitment.timestamp).abs() > 30 { + return Err(Error::new( + eyre!("timestamp not within 30s of now"), + ErrorKind::InvalidSignature, + )); + } + self.handle_nonce(commitment.nonce).await?; + + let mut body = Vec::with_capacity(commitment.size as usize); + commitment.copy_to(request, &mut body).await?; + *request.body_mut() = Body::from(body); + + Ok(signer) + } + .await + .map_err(RpcError::from), + ); + } + Ok(()) + } + async fn process_rpc_request( + &mut self, + ctx: &RegistryContext, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + async move { + let signer = self.signer.take().transpose()?; + if metadata.get_signer { + if let Some(signer) = &signer { + request.params["__auth_signer"] = to_value(signer)?; + } + } + if metadata.admin { + let signer = signer + .ok_or_else(|| Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))?; + let db = ctx.db.peek().await; + let (guid, admin) = db.as_index().as_signers().get_signer_info(&signer)?; + if db.into_admins().de()?.contains(&guid) { + let mut log = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(ctx.datadir.join("admin.log")) + .await?; + log.write_all( + (serde_json::to_string(&RegistryAdminLogRecord { + timestamp: Utc::now().to_rfc3339(), + name: admin.name, + request: request.clone(), + key: signer, + }) + .with_kind(ErrorKind::Serialization)? + + "\n") + .as_bytes(), + ) + .await?; + } else { + return Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)); + } + } + + Ok(()) + } + .await + .map_err(|e| RpcResponse::from_result(Err(e))) + } +} diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs new file mode 100644 index 000000000..d78fde50e --- /dev/null +++ b/core/startos/src/registry/context.rs @@ -0,0 +1,288 @@ +use std::net::{Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use imbl_value::InternedString; +use patch_db::PatchDb; +use reqwest::{Client, Proxy}; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{CallRemote, Context, Empty}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use tokio::sync::broadcast::Sender; +use tracing::instrument; +use url::Url; + +use crate::context::config::{ContextConfig, CONFIG_PATH}; +use crate::context::{CliContext, RpcContext}; +use crate::prelude::*; +use crate::registry::auth::{SignatureHeader, AUTH_SIG_HEADER}; +use crate::registry::signer::sign::AnySigningKey; +use crate::registry::RegistryDatabase; +use crate::rpc_continuations::RpcContinuations; + +#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct RegistryConfig { + #[arg(short = 'c', long = "config")] + pub config: Option, + #[arg(short = 'l', long = "listen")] + pub listen: Option, + #[arg(short = 'h', long = "hostname")] + pub hostname: Option, + #[arg(short = 'p', long = "tor-proxy")] + pub tor_proxy: Option, + #[arg(short = 'd', long = "datadir")] + pub datadir: Option, + #[arg(short = 'u', long = "pg-connection-url")] + pub pg_connection_url: Option, +} +impl ContextConfig for RegistryConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.listen = self.listen.take().or(other.listen); + self.hostname = self.hostname.take().or(other.hostname); + self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy); + self.datadir = self.datadir.take().or(other.datadir); + } +} + +impl RegistryConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(Some(CONFIG_PATH))?; + Ok(self) + } +} + +pub struct RegistryContextSeed { + pub hostname: InternedString, + pub listen: SocketAddr, + pub db: TypedPatchDb, + pub datadir: PathBuf, + pub rpc_continuations: RpcContinuations, + pub client: Client, + pub shutdown: Sender<()>, + pub pool: Option, +} + +#[derive(Clone)] +pub struct RegistryContext(Arc); +impl RegistryContext { + #[instrument(skip_all)] + pub async fn init(config: &RegistryConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + let datadir = config + .datadir + .as_deref() + .unwrap_or_else(|| Path::new("/var/lib/startos")) + .to_owned(); + if tokio::fs::metadata(&datadir).await.is_err() { + tokio::fs::create_dir_all(&datadir).await?; + } + let db_path = datadir.join("registry.db"); + let db = TypedPatchDb::::load_or_init( + PatchDb::open(&db_path).await?, + || async { Ok(Default::default()) }, + ) + .await?; + let tor_proxy_url = config + .tor_proxy + .clone() + .map(Ok) + .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; + let pool: Option = match &config.pg_connection_url { + Some(url) => match PgPool::connect(url.as_str()).await { + Ok(pool) => Some(pool), + Err(_) => None, + }, + None => None, + }; + Ok(Self(Arc::new(RegistryContextSeed { + hostname: config + .hostname + .as_ref() + .ok_or_else(|| { + Error::new( + eyre!("missing required configuration: hostname"), + ErrorKind::NotFound, + ) + })? + .clone(), + listen: config + .listen + .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), + db, + datadir, + rpc_continuations: RpcContinuations::new(), + client: Client::builder() + .proxy(Proxy::custom(move |url| { + if url.host_str().map_or(false, |h| h.ends_with(".onion")) { + Some(tor_proxy_url.clone()) + } else { + None + } + })) + .build() + .with_kind(crate::ErrorKind::ParseUrl)?, + shutdown, + pool, + }))) + } +} +impl AsRef for RegistryContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for RegistryContext {} +impl Deref for RegistryContext { + type Target = RegistryContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +pub struct RegistryUrlParams { + pub registry: Url, +} + +impl CallRemote for CliContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + _: Empty, + ) -> Result { + use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; + use reqwest::Method; + use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; + use rpc_toolkit::RpcResponse; + + let url = self + .registry_url + .clone() + .ok_or_else(|| Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest))?; + method = method.strip_prefix("registry.").unwrap_or(method); + + let rpc_req = RpcRequest { + id: Some(Id::Number(0.into())), + method: GenericRpcMethod::<_, _, Value>::new(method), + params, + }; + let body = serde_json::to_vec(&rpc_req)?; + let host = url.host().or_not_found("registry hostname")?.to_string(); + let res = self + .client + .request(Method::POST, url) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(CONTENT_LENGTH, body.len()) + .header( + AUTH_SIG_HEADER, + SignatureHeader::sign( + &AnySigningKey::Ed25519(self.developer_key()?.clone()), + &body, + &host, + )? + .to_header(), + ) + .body(body) + .send() + .await?; + + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + + match res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some("application/json") => { + serde_json::from_slice::(&*res.bytes().await?) + .with_kind(ErrorKind::Deserialization)? + .result + } + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), + } + } +} + +impl CallRemote for RpcContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + RegistryUrlParams { registry }: RegistryUrlParams, + ) -> Result { + use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; + use reqwest::Method; + use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; + use rpc_toolkit::RpcResponse; + + let url = registry.join("rpc/v0")?; + method = method.strip_prefix("registry.").unwrap_or(method); + + let rpc_req = RpcRequest { + id: Some(Id::Number(0.into())), + method: GenericRpcMethod::<_, _, Value>::new(method), + params, + }; + let body = serde_json::to_vec(&rpc_req)?; + let res = self + .client + .request(Method::POST, url) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(CONTENT_LENGTH, body.len()) + // .header(DEVICE_INFO_HEADER, DeviceInfo::from(self).to_header_value()) + .body(body) + .send() + .await?; + + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + + match res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some("application/json") => { + serde_json::from_slice::(&*res.bytes().await?) + .with_kind(ErrorKind::Deserialization)? + .result + } + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), + } + } +} diff --git a/core/startos/src/registry/db.rs b/core/startos/src/registry/db.rs new file mode 100644 index 000000000..8de9f8743 --- /dev/null +++ b/core/startos/src/registry/db.rs @@ -0,0 +1,181 @@ +use std::path::PathBuf; + +use clap::Parser; +use itertools::Itertools; +use patch_db::json_ptr::{JsonPointer, ROOT}; +use patch_db::Dump; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::RegistryDatabase; +use crate::util::serde::{apply_expr, HandlerExtSerde}; + +pub fn db_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "dump", + from_fn_async(cli_dump) + .with_display_serializable() + .with_about("Filter/query db to display tables and records"), + ) + .subcommand( + "dump", + from_fn_async(dump) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "apply", + from_fn_async(cli_apply) + .no_display() + .with_about("Update a db record"), + ) + .subcommand( + "apply", + from_fn_async(apply) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliDumpParams { + #[arg(long = "pointer", short = 'p')] + pointer: Option, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_dump( + HandlerArgs { + context, + parent_method, + method, + params: CliDumpParams { pointer, path }, + .. + }: HandlerArgs, +) -> Result { + let dump = if let Some(path) = path { + PatchDb::open(path).await?.dump(&ROOT).await + } else { + let method = parent_method.into_iter().chain(method).join("."); + from_value::( + context + .call_remote::(&method, imbl_value::json!({ "pointer": pointer })) + .await?, + )? + }; + + Ok(dump) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DumpParams { + #[arg(long = "pointer", short = 'p')] + #[ts(type = "string | null")] + pointer: Option, +} + +pub async fn dump(ctx: RegistryContext, DumpParams { pointer }: DumpParams) -> Result { + Ok(ctx + .db + .dump(&pointer.as_ref().map_or(ROOT, |p| p.borrowed())) + .await) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliApplyParams { + expr: String, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_apply( + HandlerArgs { + context, + parent_method, + method, + params: CliApplyParams { expr, path }, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { + if let Some(path) = path { + PatchDb::open(path) + .await? + .apply_function(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db)) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + Ok::<_, Error>(( + to_value( + &serde_json::from_value::(res.clone().into()).with_ctx( + |_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + }, + )?, + )?, + (), + )) + }) + .await?; + } else { + let method = parent_method.into_iter().chain(method).join("."); + context + .call_remote::(&method, imbl_value::json!({ "expr": expr })) + .await?; + } + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ApplyParams { + expr: String, + path: Option, +} + +pub async fn apply( + ctx: RegistryContext, + ApplyParams { expr, .. }: ApplyParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db.clone())) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + db.ser( + &serde_json::from_value::(res.clone().into()).with_ctx(|_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + })?, + ) + }) + .await +} diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs new file mode 100644 index 000000000..410e45f8f --- /dev/null +++ b/core/startos/src/registry/device_info.rs @@ -0,0 +1,188 @@ +use std::collections::BTreeMap; +use std::convert::identity; +use std::ops::Deref; + +use axum::extract::Request; +use axum::response::Response; +use exver::{Version, VersionRange}; +use http::HeaderValue; +use imbl_value::InternedString; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::RpcContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor}; +use crate::util::VersionString; +use crate::version::VersionT; + +pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info"; + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DeviceInfo { + pub os: OsInfo, + pub hardware: HardwareInfo, +} +impl DeviceInfo { + pub async fn load(ctx: &RpcContext) -> Result { + Ok(Self { + os: OsInfo::from(ctx), + hardware: HardwareInfo::load(ctx).await?, + }) + } +} +impl DeviceInfo { + pub fn to_header_value(&self) -> HeaderValue { + let mut url: Url = "http://localhost".parse().unwrap(); + url.query_pairs_mut() + .append_pair("os.version", &self.os.version.to_string()) + .append_pair("os.compat", &self.os.compat.to_string()) + .append_pair("os.platform", &*self.os.platform) + .append_pair("hardware.arch", &*self.hardware.arch) + .append_pair("hardware.ram", &self.hardware.ram.to_string()); + + for device in &self.hardware.devices { + url.query_pairs_mut().append_pair( + &format!("hardware.device.{}", device.class()), + device.product(), + ); + } + + HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() + } + pub fn from_header_value(header: &HeaderValue) -> Result { + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); + Ok(Self { + os: OsInfo { + version: query + .get("os.version") + .or_not_found("os.version")? + .parse()?, + compat: query.get("os.compat").or_not_found("os.compat")?.parse()?, + platform: query + .get("os.platform") + .or_not_found("os.platform")? + .deref() + .into(), + }, + hardware: HardwareInfo { + arch: query + .get("hardware.arch") + .or_not_found("hardware.arch")? + .parse()?, + ram: query + .get("hardware.ram") + .or_not_found("hardware.ram")? + .parse()?, + devices: identity(query) + .split_off("hardware.device.") + .into_iter() + .filter_map(|(k, v)| match k.strip_prefix("hardware.device.") { + Some("processor") => Some(LshwDevice::Processor(LshwProcessor { + product: v.into_owned(), + })), + Some("display") => Some(LshwDevice::Display(LshwDisplay { + product: v.into_owned(), + })), + Some(class) => { + tracing::warn!("unknown device class: {class}"); + None + } + _ => None, + }) + .collect(), + }, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct OsInfo { + #[ts(as = "VersionString")] + pub version: Version, + #[ts(type = "string")] + pub compat: VersionRange, + #[ts(type = "string")] + pub platform: InternedString, +} +impl From<&RpcContext> for OsInfo { + fn from(_: &RpcContext) -> Self { + Self { + version: crate::version::Current::default().semver(), + compat: crate::version::Current::default().compat().clone(), + platform: InternedString::intern(&*crate::PLATFORM), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct HardwareInfo { + #[ts(type = "string")] + pub arch: InternedString, + #[ts(type = "number")] + pub ram: u64, + pub devices: Vec, +} +impl HardwareInfo { + pub async fn load(ctx: &RpcContext) -> Result { + let s = ctx.db.peek().await.into_public().into_server_info(); + Ok(Self { + arch: s.as_arch().de()?, + ram: s.as_ram().de()?, + devices: s.as_devices().de()?, + }) + } +} + +#[derive(Deserialize)] +pub struct Metadata { + #[serde(default)] + get_device_info: bool, +} + +#[derive(Clone)] +pub struct DeviceInfoMiddleware { + device_info: Option, +} +impl DeviceInfoMiddleware { + pub fn new() -> Self { + Self { device_info: None } + } +} + +impl Middleware for DeviceInfoMiddleware { + type Metadata = Metadata; + async fn process_http_request( + &mut self, + _: &RegistryContext, + request: &mut Request, + ) -> Result<(), Response> { + self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER); + Ok(()) + } + async fn process_rpc_request( + &mut self, + _: &RegistryContext, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + async move { + if metadata.get_device_info { + if let Some(device_info) = &self.device_info { + request.params["__device_info"] = + to_value(&DeviceInfo::from_header_value(device_info)?)?; + } + } + + Ok::<_, Error>(()) + } + .await + .map_err(|e| RpcResponse::from_result(Err(e))) + } +} diff --git a/core/startos/src/registry/info.rs b/core/startos/src/registry/info.rs new file mode 100644 index 000000000..402f0891a --- /dev/null +++ b/core/startos/src/registry/info.rs @@ -0,0 +1,126 @@ +use std::collections::BTreeMap; +use std::path::PathBuf; + +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use models::DataUrl; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::package::index::Category; +use crate::util::serde::{HandlerExtSerde, WithIoFormat}; + +pub fn info_api() -> ParentHandler> { + ParentHandler::>::new() + .root_handler( + from_fn_async(get_info) + .with_display_serializable() + .with_about("Display registry name, icon, and package categories") + .with_call_remote::(), + ) + .subcommand( + "set-name", + from_fn_async(set_name) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Set the name for the registry") + .with_call_remote::(), + ) + .subcommand( + "set-icon", + from_fn_async(set_icon) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "set-icon", + from_fn_async(cli_set_icon) + .no_display() + .with_about("Set the icon for the registry"), + ) +} + +#[derive(Debug, Default, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RegistryInfo { + pub name: Option, + pub icon: Option>, + #[ts(as = "BTreeMap::")] + pub categories: BTreeMap, +} + +pub async fn get_info(ctx: RegistryContext) -> Result { + let peek = ctx.db.peek().await.into_index(); + Ok(RegistryInfo { + name: peek.as_name().de()?, + icon: peek.as_icon().de()?, + categories: peek.as_package().as_categories().de()?, + }) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetNameParams { + pub name: String, +} + +pub async fn set_name( + ctx: RegistryContext, + SetNameParams { name }: SetNameParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| db.as_index_mut().as_name_mut().ser(&Some(name))) + .await +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetIconParams { + pub icon: DataUrl<'static>, +} + +pub async fn set_icon( + ctx: RegistryContext, + SetIconParams { icon }: SetIconParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| db.as_index_mut().as_icon_mut().ser(&Some(icon))) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CliSetIconParams { + pub icon: PathBuf, +} + +pub async fn cli_set_icon( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliSetIconParams { icon }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let data_url = DataUrl::from_path(icon).await?; + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + imbl_value::json!({ + "icon": data_url, + }), + ) + .await?; + Ok(()) +} diff --git a/core/startos/src/registry/marketplace.rs b/core/startos/src/registry/marketplace.rs deleted file mode 100644 index 979733198..000000000 --- a/core/startos/src/registry/marketplace.rs +++ /dev/null @@ -1,92 +0,0 @@ -use base64::Engine; -use color_eyre::eyre::eyre; -use reqwest::{StatusCode, Url}; -use rpc_toolkit::command; -use serde_json::Value; - -use crate::context::RpcContext; -use crate::version::VersionT; -use crate::{Error, ResultExt}; - -#[command(subcommands(get))] -pub fn marketplace() -> Result<(), Error> { - Ok(()) -} - -pub fn with_query_params(ctx: RpcContext, mut url: Url) -> Url { - url.query_pairs_mut() - .append_pair( - "os.version", - &crate::version::Current::new().semver().to_string(), - ) - .append_pair( - "os.compat", - &crate::version::Current::new().compat().to_string(), - ) - .append_pair("os.arch", &*crate::PLATFORM) - .append_pair("hardware.arch", &*crate::ARCH) - .append_pair("hardware.ram", &ctx.hardware.ram.to_string()); - - for hw in &ctx.hardware.devices { - url.query_pairs_mut() - .append_pair(&format!("hardware.device.{}", hw.class()), hw.product()); - } - - url -} - -#[command] -pub async fn get(#[context] ctx: RpcContext, #[arg] url: Url) -> Result { - let mut response = ctx - .client - .get(with_query_params(ctx.clone(), url)) - .send() - .await - .with_kind(crate::ErrorKind::Network)?; - let status = response.status(); - if status.is_success() { - match response - .headers_mut() - .remove("Content-Type") - .as_ref() - .and_then(|h| h.to_str().ok()) - .and_then(|h| h.split(";").next()) - .map(|h| h.trim()) - { - Some("application/json") => response - .json() - .await - .with_kind(crate::ErrorKind::Deserialization), - Some("text/plain") => Ok(Value::String( - response - .text() - .await - .with_kind(crate::ErrorKind::Registry)?, - )), - Some(ctype) => Ok(Value::String(format!( - "data:{};base64,{}", - ctype, - base64::engine::general_purpose::URL_SAFE.encode( - &response - .bytes() - .await - .with_kind(crate::ErrorKind::Registry)? - ) - ))), - _ => Err(Error::new( - eyre!("missing Content-Type"), - crate::ErrorKind::Registry, - )), - } - } else { - let message = response.text().await.with_kind(crate::ErrorKind::Network)?; - Err(Error::new( - eyre!("{}", message), - match status { - StatusCode::BAD_REQUEST => crate::ErrorKind::InvalidRequest, - StatusCode::NOT_FOUND => crate::ErrorKind::NotFound, - _ => crate::ErrorKind::Registry, - }, - )) - } -} diff --git a/core/startos/src/registry/metrics-db/registry-sqlx-data.sh b/core/startos/src/registry/metrics-db/registry-sqlx-data.sh new file mode 100755 index 000000000..2b24873a4 --- /dev/null +++ b/core/startos/src/registry/metrics-db/registry-sqlx-data.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" +TMP_DIR=$(mktemp -d) +mkdir $TMP_DIR/pgdata +docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres + +( + set -e + ctr=0 + until docker exec tmp_postgres psql -U postgres 2> /dev/null || [ $ctr -ge 5 ]; do + ctr=$[ctr + 1] + sleep 5; + done + + PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres) + + cat "./registry_schema.sql" | docker exec -i tmp_postgres psql -U postgres -d postgres -f- + cd ../../.. + DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace + echo "Subscript Complete" +) + +docker stop tmp_postgres +sudo rm -rf $TMP_DIR diff --git a/core/startos/src/registry/metrics-db/registry_schema.sql b/core/startos/src/registry/metrics-db/registry_schema.sql new file mode 100644 index 000000000..abd9f4ea6 --- /dev/null +++ b/core/startos/src/registry/metrics-db/registry_schema.sql @@ -0,0 +1,828 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1) +-- Dumped by pg_dump version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: admin; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.admin ( + id character varying NOT NULL, + created_at timestamp with time zone NOT NULL, + pass_hash character varying NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.admin OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.admin_pkgs ( + id bigint NOT NULL, + admin character varying NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.admin_pkgs OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.admin_pkgs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.admin_pkgs_id_seq OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.admin_pkgs_id_seq OWNED BY public.admin_pkgs.id; + + +-- +-- Name: category; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.category ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + name character varying NOT NULL, + description character varying NOT NULL, + priority bigint DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.category OWNER TO alpha_admin; + +-- +-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.category_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.category_id_seq OWNER TO alpha_admin; + +-- +-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id; + + +-- +-- Name: eos_hash; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.eos_hash ( + id bigint NOT NULL, + version character varying NOT NULL, + hash character varying NOT NULL +); + + +ALTER TABLE public.eos_hash OWNER TO alpha_admin; + +-- +-- Name: eos_hash_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.eos_hash_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.eos_hash_id_seq OWNER TO alpha_admin; + +-- +-- Name: eos_hash_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.eos_hash_id_seq OWNED BY public.eos_hash.id; + + +-- +-- Name: error_log_record; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.error_log_record ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + epoch character varying NOT NULL, + commit_hash character varying NOT NULL, + source_file character varying NOT NULL, + line bigint NOT NULL, + target character varying NOT NULL, + level character varying NOT NULL, + message character varying NOT NULL, + incidents bigint NOT NULL +); + + +ALTER TABLE public.error_log_record OWNER TO alpha_admin; + +-- +-- Name: error_log_record_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.error_log_record_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.error_log_record_id_seq OWNER TO alpha_admin; + +-- +-- Name: error_log_record_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.error_log_record_id_seq OWNED BY public.error_log_record.id; + + +-- +-- Name: metric; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.metric ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + version character varying NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.metric OWNER TO alpha_admin; + +-- +-- Name: metric_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.metric_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.metric_id_seq OWNER TO alpha_admin; + +-- +-- Name: metric_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.metric_id_seq OWNED BY public.metric.id; + + +-- +-- Name: os_version; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.os_version ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + number character varying NOT NULL, + headline character varying NOT NULL, + release_notes character varying NOT NULL, + arch character varying +); + + +ALTER TABLE public.os_version OWNER TO alpha_admin; + +-- +-- Name: os_version_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.os_version_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.os_version_id_seq OWNER TO alpha_admin; + +-- +-- Name: os_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.os_version_id_seq OWNED BY public.os_version.id; + + +-- +-- Name: persistent_migration; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.persistent_migration ( + id integer NOT NULL, + version integer NOT NULL, + label character varying, + "timestamp" timestamp with time zone NOT NULL +); + + +ALTER TABLE public.persistent_migration OWNER TO alpha_admin; + +-- +-- Name: persistent_migration_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.persistent_migration_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.persistent_migration_id_seq OWNER TO alpha_admin; + +-- +-- Name: persistent_migration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.persistent_migration_id_seq OWNED BY public.persistent_migration.id; + + +-- +-- Name: pkg_category; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_category ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + category_id bigint NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.pkg_category OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_dependency ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + pkg_id character varying NOT NULL, + pkg_version character varying NOT NULL, + dep_id character varying NOT NULL, + dep_version_range character varying NOT NULL +); + + +ALTER TABLE public.pkg_dependency OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.pkg_dependency_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.pkg_dependency_id_seq OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.pkg_dependency_id_seq OWNED BY public.pkg_dependency.id; + + +-- +-- Name: pkg_record; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_record ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + pkg_id character varying NOT NULL, + hidden boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.pkg_record OWNER TO alpha_admin; + +-- +-- Name: service_category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.service_category_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.service_category_id_seq OWNER TO alpha_admin; + +-- +-- Name: service_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.service_category_id_seq OWNED BY public.pkg_category.id; + + +-- +-- Name: upload; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.upload ( + id bigint NOT NULL, + uploader character varying NOT NULL, + pkg_id character varying NOT NULL, + pkg_version character varying NOT NULL, + created_at timestamp with time zone NOT NULL +); + + +ALTER TABLE public.upload OWNER TO alpha_admin; + +-- +-- Name: upload_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.upload_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.upload_id_seq OWNER TO alpha_admin; + +-- +-- Name: upload_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.upload_id_seq OWNED BY public.upload.id; + + +-- +-- Name: user_activity; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.user_activity ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + server_id character varying NOT NULL, + os_version character varying, + arch character varying +); + + +ALTER TABLE public.user_activity OWNER TO alpha_admin; + +-- +-- Name: user_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.user_activity_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.user_activity_id_seq OWNER TO alpha_admin; + +-- +-- Name: user_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.user_activity_id_seq OWNED BY public.user_activity.id; + + +-- +-- Name: version; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.version ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + number character varying NOT NULL, + release_notes character varying NOT NULL, + os_version character varying NOT NULL, + pkg_id character varying NOT NULL, + title character varying NOT NULL, + desc_short character varying NOT NULL, + desc_long character varying NOT NULL, + icon_type character varying NOT NULL, + deprecated_at timestamp with time zone +); + + +ALTER TABLE public.version OWNER TO alpha_admin; + +-- +-- Name: version_platform; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.version_platform ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + pkg_id character varying NOT NULL, + version_number character varying NOT NULL, + arch character varying NOT NULL, + ram bigint, + device jsonb +); + + +ALTER TABLE public.version_platform OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs ALTER COLUMN id SET DEFAULT nextval('public.admin_pkgs_id_seq'::regclass); + + +-- +-- Name: category id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass); + + +-- +-- Name: eos_hash id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash ALTER COLUMN id SET DEFAULT nextval('public.eos_hash_id_seq'::regclass); + + +-- +-- Name: error_log_record id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record ALTER COLUMN id SET DEFAULT nextval('public.error_log_record_id_seq'::regclass); + + +-- +-- Name: metric id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric ALTER COLUMN id SET DEFAULT nextval('public.metric_id_seq'::regclass); + + +-- +-- Name: os_version id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.os_version ALTER COLUMN id SET DEFAULT nextval('public.os_version_id_seq'::regclass); + + +-- +-- Name: persistent_migration id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.persistent_migration ALTER COLUMN id SET DEFAULT nextval('public.persistent_migration_id_seq'::regclass); + + +-- +-- Name: pkg_category id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category ALTER COLUMN id SET DEFAULT nextval('public.service_category_id_seq'::regclass); + + +-- +-- Name: pkg_dependency id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency ALTER COLUMN id SET DEFAULT nextval('public.pkg_dependency_id_seq'::regclass); + + +-- +-- Name: upload id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload ALTER COLUMN id SET DEFAULT nextval('public.upload_id_seq'::regclass); + + +-- +-- Name: user_activity id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.user_activity ALTER COLUMN id SET DEFAULT nextval('public.user_activity_id_seq'::regclass); + + +-- +-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin + ADD CONSTRAINT admin_pkey PRIMARY KEY (id); + + +-- +-- Name: admin_pkgs admin_pkgs_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT admin_pkgs_pkey PRIMARY KEY (id); + + +-- +-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category + ADD CONSTRAINT category_pkey PRIMARY KEY (id); + + +-- +-- Name: eos_hash eos_hash_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash + ADD CONSTRAINT eos_hash_pkey PRIMARY KEY (id); + + +-- +-- Name: error_log_record error_log_record_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record + ADD CONSTRAINT error_log_record_pkey PRIMARY KEY (id); + + +-- +-- Name: metric metric_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric + ADD CONSTRAINT metric_pkey PRIMARY KEY (id); + + +-- +-- Name: os_version os_version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.os_version + ADD CONSTRAINT os_version_pkey PRIMARY KEY (id); + + +-- +-- Name: persistent_migration persistent_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.persistent_migration + ADD CONSTRAINT persistent_migration_pkey PRIMARY KEY (id); + + +-- +-- Name: pkg_category pkg_category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_pkey PRIMARY KEY (id); + + +-- +-- Name: pkg_dependency pkg_dependency_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_pkey PRIMARY KEY (id); + + +-- +-- Name: admin_pkgs unique_admin_pkg; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT unique_admin_pkg UNIQUE (pkg_id, admin); + + +-- +-- Name: error_log_record unique_log_record; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record + ADD CONSTRAINT unique_log_record UNIQUE (epoch, commit_hash, source_file, line, target, level, message); + + +-- +-- Name: category unique_name; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category + ADD CONSTRAINT unique_name UNIQUE (name); + + +-- +-- Name: pkg_category unique_pkg_category; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT unique_pkg_category UNIQUE (pkg_id, category_id); + + +-- +-- Name: pkg_dependency unique_pkg_dep_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT unique_pkg_dep_version UNIQUE (pkg_id, pkg_version, dep_id); + + +-- +-- Name: eos_hash unique_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash + ADD CONSTRAINT unique_version UNIQUE (version); + + +-- +-- Name: upload upload_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_pkey PRIMARY KEY (id); + + +-- +-- Name: user_activity user_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.user_activity + ADD CONSTRAINT user_activity_pkey PRIMARY KEY (id); + + +-- +-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version + ADD CONSTRAINT version_pkey PRIMARY KEY (pkg_id, number); + + +-- +-- Name: version_platform version_platform_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version_platform + ADD CONSTRAINT version_platform_pkey PRIMARY KEY (pkg_id, version_number, arch); + + +-- +-- Name: category_name_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE UNIQUE INDEX category_name_idx ON public.category USING btree (name); + + +-- +-- Name: pkg_record_pkg_id_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE UNIQUE INDEX pkg_record_pkg_id_idx ON public.pkg_record USING btree (pkg_id); + + +-- +-- Name: version_number_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE INDEX version_number_idx ON public.version USING btree (number); + + +-- +-- Name: admin_pkgs admin_pkgs_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT admin_pkgs_admin_fkey FOREIGN KEY (admin) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: metric metric_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric + ADD CONSTRAINT metric_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_category pkg_category_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_category pkg_category_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_dependency pkg_dependency_dep_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_dep_id_fkey FOREIGN KEY (dep_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_dependency pkg_dependency_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: upload upload_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: upload upload_uploader_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_uploader_fkey FOREIGN KEY (uploader) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: version version_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version + ADD CONSTRAINT version_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: version_platform version_platform_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version_platform + ADD CONSTRAINT version_platform_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 27f541f1d..4e1411ea9 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -1,2 +1,150 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use axum::Router; +use futures::future::ready; +use models::DataUrl; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::middleware::cors::Cors; +use crate::net::static_server::{bad_request, not_found, server_error}; +use crate::net::web_server::{Accept, WebServer}; +use crate::prelude::*; +use crate::registry::auth::Auth; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfoMiddleware; +use crate::registry::os::index::OsIndex; +use crate::registry::package::index::PackageIndex; +use crate::registry::signer::SignerInfo; +use crate::rpc_continuations::Guid; +use crate::util::serde::HandlerExtSerde; + pub mod admin; -pub mod marketplace; +pub mod asset; +pub mod auth; +pub mod context; +pub mod db; +pub mod device_info; +pub mod info; +pub mod os; +pub mod package; +pub mod signer; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct RegistryDatabase { + pub admins: BTreeSet, + pub index: FullIndex, +} +impl RegistryDatabase {} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct FullIndex { + pub name: Option, + pub icon: Option>, + pub package: PackageIndex, + pub os: OsIndex, + pub signers: BTreeMap, +} + +pub async fn get_full_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().de() +} + +pub fn registry_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(get_full_index) + .with_display_serializable() + .with_about("List info including registry name and packages") + .with_call_remote::(), + ) + .subcommand("info", info::info_api::()) + // set info and categories + .subcommand( + "os", + os::os_api::().with_about("Commands related to OS assets and versions"), + ) + .subcommand( + "package", + package::package_api::().with_about("Commands to index, add, or get packages"), + ) + .subcommand( + "admin", + admin::admin_api::().with_about("Commands to add or list admins or signers"), + ) + .subcommand( + "db", + db::db_api::().with_about("Commands to interact with the db i.e. dump and apply"), + ) +} + +pub fn registry_router(ctx: RegistryContext) -> Router { + use axum::extract as x; + use axum::routing::{any, get}; + Router::new() + .route("/rpc/*path", { + let ctx = ctx.clone(); + any( + Server::new(move || ready(Ok(ctx.clone())), registry_api()) + .middleware(Cors::new()) + .middleware(Auth::new()) + .middleware(DeviceInfoMiddleware::new()), + ) + }) + .route( + "/ws/rpc/*path", + get({ + let ctx = ctx.clone(); + move |x::Path(path): x::Path, + ws: axum::extract::ws::WebSocketUpgrade| async move { + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), + }, + } + } + }), + ) + .route( + "/rest/rpc/*path", + any({ + let ctx = ctx.clone(); + move |request: x::Request| async move { + let path = request + .uri() + .path() + .strip_prefix("/rest/rpc/") + .unwrap_or_default(); + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), + }, + } + } + }), + ) +} + +impl WebServer { + pub fn serve_registry(&mut self, ctx: RegistryContext) { + self.serve_router(registry_router(ctx)) + } +} diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs new file mode 100644 index 000000000..d609063ea --- /dev/null +++ b/core/startos/src/registry/os/asset/add.rs @@ -0,0 +1,257 @@ +use std::collections::{BTreeMap, HashMap}; +use std::panic::UnwindSafe; +use std::path::PathBuf; + +use chrono::Utc; +use clap::Parser; +use exver::Version; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::open_file; +use crate::util::serde::Base64; + +pub fn add_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "iso", + from_fn_async(add_iso) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "img", + from_fn_async(add_img) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "squashfs", + from_fn_async(add_squashfs) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddAssetParams { + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub platform: InternedString, + #[ts(type = "string")] + pub url: Url, + #[serde(rename = "__auth_signer")] + #[ts(skip)] + pub signer: AnyVerifyingKey, + pub signature: AnySignature, + pub commitment: Blake3Commitment, +} + +async fn add_asset( + ctx: RegistryContext, + AddAssetParams { + version, + platform, + url, + signer, + signature, + commitment, + }: AddAssetParams, + accessor: impl FnOnce( + &mut Model, + ) -> &mut Model>> + + UnwindSafe + + Send, +) -> Result<(), Error> { + signer + .scheme() + .verify_commitment(&signer, &commitment, SIG_CONTEXT, &signature)?; + ctx.db + .mutate(|db| { + let signer_guid = db.as_index().as_signers().get_signer(&signer)?; + if db + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_authorized() + .de()? + .contains(&signer_guid) + { + accessor( + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)?, + ) + .upsert(&platform, || { + Ok(RegistryAsset { + published_at: Utc::now(), + url, + commitment: commitment.clone(), + signatures: HashMap::new(), + }) + })? + .mutate(|s| { + if s.commitment != commitment { + Err(Error::new( + eyre!("commitment does not match"), + ErrorKind::InvalidSignature, + )) + } else { + s.signatures.insert(signer, signature); + Ok(()) + } + })?; + Ok(()) + } else { + Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) + } + }) + .await?; + + Ok(()) +} + +pub async fn add_iso(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { + add_asset(ctx, params, |m| m.as_iso_mut()).await +} + +pub async fn add_img(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { + add_asset(ctx, params, |m| m.as_img_mut()).await +} + +pub async fn add_squashfs(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { + add_asset(ctx, params, |m| m.as_squashfs_mut()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddAssetParams { + #[arg(short = 'p', long = "platform")] + pub platform: InternedString, + #[arg(short = 'v', long = "version")] + pub version: Version, + pub file: PathBuf, + pub url: Url, +} + +pub async fn cli_add_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliAddAssetParams { + platform, + version, + file: path, + url, + }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let ext = match path.extension().and_then(|e| e.to_str()) { + Some("iso") => "iso", + Some("img") => "img", + Some("squashfs") => "squashfs", + _ => { + return Err(Error::new( + eyre!("Unknown extension"), + ErrorKind::InvalidRequest, + )) + } + }; + + let file = MultiCursorFile::from(open_file(&path).await?); + + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100)); + let mut index_phase = progress.add_phase( + InternedString::intern("Adding File to Registry Index"), + Some(1), + ); + + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", path.display())); + + sign_phase.start(); + let blake3 = file.blake3_mmap().await?; + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?; + let commitment = Blake3Commitment { + hash: Base64(*blake3.as_bytes()), + size, + }; + let signature = AnySignature::Ed25519(Ed25519.sign_commitment( + ctx.developer_key()?, + &commitment, + SIG_CONTEXT, + )?); + sign_phase.complete(); + + verify_phase.start(); + let src = HttpSource::new(ctx.client.clone(), url.clone()).await?; + if let Some(size) = src.size().await { + verify_phase.set_total(size); + } + let mut writer = verify_phase.writer(VerifyingWriter::new( + tokio::io::sink(), + Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)), + )); + src.copy_all_to(&mut writer).await?; + let (verifier, mut verify_phase) = writer.into_inner(); + verifier.verify().await?; + verify_phase.complete(); + + index_phase.start(); + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method) + .chain([ext]) + .join("."), + imbl_value::json!({ + "platform": platform, + "version": version, + "url": &url, + "signature": signature, + "commitment": commitment, + }), + ) + .await?; + index_phase.complete(); + + progress.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs new file mode 100644 index 000000000..a3da7047c --- /dev/null +++ b/core/startos/src/registry/os/asset/get.rs @@ -0,0 +1,200 @@ +use std::collections::BTreeMap; +use std::panic::UnwindSafe; +use std::path::{Path, PathBuf}; + +use clap::Parser; +use exver::Version; +use helpers::AtomicFile; +use imbl_value::{json, InternedString}; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::commitment::Commitment; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::util::io::open_file; + +pub fn get_api() -> ParentHandler { + ParentHandler::new() + .subcommand("iso", from_fn_async(get_iso).no_cli()) + .subcommand( + "iso", + from_fn_async(cli_get_os_asset) + .no_display() + .with_about("Download iso"), + ) + .subcommand("img", from_fn_async(get_img).no_cli()) + .subcommand( + "img", + from_fn_async(cli_get_os_asset) + .no_display() + .with_about("Download img"), + ) + .subcommand("squashfs", from_fn_async(get_squashfs).no_cli()) + .subcommand( + "squashfs", + from_fn_async(cli_get_os_asset) + .no_display() + .with_about("Download squashfs"), + ) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetOsAssetParams { + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub platform: InternedString, +} + +async fn get_os_asset( + ctx: RegistryContext, + GetOsAssetParams { version, platform }: GetOsAssetParams, + accessor: impl FnOnce( + &Model, + ) -> &Model>> + + UnwindSafe + + Send, +) -> Result, Error> { + accessor( + ctx.db + .peek() + .await + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)?, + ) + .as_idx(&platform) + .or_not_found(&platform)? + .de() +} + +pub async fn get_iso( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result, Error> { + get_os_asset(ctx, params, |info| info.as_iso()).await +} + +pub async fn get_img( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result, Error> { + get_os_asset(ctx, params, |info| info.as_img()).await +} + +pub async fn get_squashfs( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result, Error> { + get_os_asset(ctx, params, |info| info.as_squashfs()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliGetOsAssetParams { + pub version: Version, + pub platform: InternedString, + #[arg( + long = "download", + short = 'd', + help = "The path of the directory to download to" + )] + pub download: Option, + #[arg( + long = "reverify", + short = 'r', + help = "verify the hash of the file a second time after download" + )] + pub reverify: bool, +} + +async fn cli_get_os_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliGetOsAssetParams { + version, + platform, + download, + reverify, + }, + .. + }: HandlerArgs, +) -> Result, Error> { + let ext = method + .iter() + .last() + .or_else(|| parent_method.iter().last()) + .unwrap_or(&"bin"); + + let res = from_value::>( + ctx.call_remote::( + &parent_method.iter().chain(&method).join("."), + json!({ + "version": version, + "platform": platform, + }), + ) + .await?, + )?; + + res.validate(SIG_CONTEXT, res.all_signers())?; + + if let Some(download) = download { + let download = download.join(format!("startos-{version}_{platform}.{ext}")); + let mut file = AtomicFile::new(&download, None::<&Path>) + .await + .with_kind(ErrorKind::Filesystem)?; + + let progress = FullProgressTracker::new(); + let mut download_phase = + progress.add_phase(InternedString::intern("Downloading File"), Some(100)); + download_phase.set_total(res.commitment.size); + let reverify_phase = if reverify { + Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10))) + } else { + None + }; + + let progress_task = progress.progress_bar_task("Downloading..."); + + download_phase.start(); + let mut download_writer = download_phase.writer(&mut *file); + res.download(ctx.client.clone(), &mut download_writer) + .await?; + let (_, mut download_phase) = download_writer.into_inner(); + file.save().await.with_kind(ErrorKind::Filesystem)?; + download_phase.complete(); + + if let Some(mut reverify_phase) = reverify_phase { + reverify_phase.start(); + res.commitment + .check(&MultiCursorFile::from(open_file(download).await?)) + .await?; + reverify_phase.complete(); + } + + progress.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + } + + Ok(res) +} diff --git a/core/startos/src/registry/os/asset/mod.rs b/core/startos/src/registry/os/asset/mod.rs new file mode 100644 index 000000000..52c12341a --- /dev/null +++ b/core/startos/src/registry/os/asset/mod.rs @@ -0,0 +1,27 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +pub mod add; +pub mod get; +pub mod sign; + +pub fn asset_api() -> ParentHandler { + ParentHandler::new() + .subcommand("add", add::add_api::()) + .subcommand( + "add", + from_fn_async(add::cli_add_asset) + .no_display() + .with_about("Add asset to registry"), + ) + .subcommand("sign", sign::sign_api::()) + .subcommand( + "sign", + from_fn_async(sign::cli_sign_asset) + .no_display() + .with_about("Sign file and add to registry index"), + ) + .subcommand( + "get", + get::get_api::().with_about("Commands to download image, iso, or squashfs files"), + ) +} diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs new file mode 100644 index 000000000..18b603daf --- /dev/null +++ b/core/startos/src/registry/os/asset/sign.rs @@ -0,0 +1,221 @@ +use std::collections::BTreeMap; +use std::panic::UnwindSafe; +use std::path::PathBuf; + +use clap::Parser; +use exver::Version; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::open_file; +use crate::util::serde::Base64; + +pub fn sign_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "iso", + from_fn_async(sign_iso) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "img", + from_fn_async(sign_img) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "squashfs", + from_fn_async(sign_squashfs) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SignAssetParams { + #[ts(type = "string")] + version: Version, + #[ts(type = "string")] + platform: InternedString, + #[ts(skip)] + #[serde(rename = "__auth_signer")] + signer: AnyVerifyingKey, + signature: AnySignature, +} + +async fn sign_asset( + ctx: RegistryContext, + SignAssetParams { + version, + platform, + signer, + signature, + }: SignAssetParams, + accessor: impl FnOnce( + &mut Model, + ) -> &mut Model>> + + UnwindSafe + + Send, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let guid = db.as_index().as_signers().get_signer(&signer)?; + if !db + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_authorized() + .de()? + .contains(&guid) + { + return Err(Error::new( + eyre!("signer {guid} is not authorized"), + ErrorKind::Authorization, + )); + } + + accessor( + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)?, + ) + .as_idx_mut(&platform) + .or_not_found(&platform)? + .mutate(|s| { + signer.scheme().verify_commitment( + &signer, + &s.commitment, + SIG_CONTEXT, + &signature, + )?; + s.signatures.insert(signer, signature); + Ok(()) + })?; + + Ok(()) + }) + .await +} + +pub async fn sign_iso(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_iso_mut()).await +} + +pub async fn sign_img(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_img_mut()).await +} + +pub async fn sign_squashfs(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_squashfs_mut()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliSignAssetParams { + #[arg(short = 'p', long = "platform")] + pub platform: InternedString, + #[arg(short = 'v', long = "version")] + pub version: Version, + pub file: PathBuf, +} + +pub async fn cli_sign_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliSignAssetParams { + platform, + version, + file: path, + }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let ext = match path.extension().and_then(|e| e.to_str()) { + Some("iso") => "iso", + Some("img") => "img", + Some("squashfs") => "squashfs", + _ => { + return Err(Error::new( + eyre!("Unknown extension"), + ErrorKind::InvalidRequest, + )) + } + }; + + let file = MultiCursorFile::from(open_file(&path).await?); + + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut index_phase = progress.add_phase( + InternedString::intern("Adding Signature to Registry Index"), + Some(1), + ); + + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", path.display())); + + sign_phase.start(); + let blake3 = file.blake3_mmap().await?; + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?; + let commitment = Blake3Commitment { + hash: Base64(*blake3.as_bytes()), + size, + }; + let signature = AnySignature::Ed25519(Ed25519.sign_commitment( + ctx.developer_key()?, + &commitment, + SIG_CONTEXT, + )?); + sign_phase.complete(); + + index_phase.start(); + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method) + .chain([ext]) + .join("."), + imbl_value::json!({ + "platform": platform, + "version": version, + "signature": signature, + }), + ) + .await?; + index_phase.complete(); + + progress.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs new file mode 100644 index 000000000..b61cb8f96 --- /dev/null +++ b/core/startos/src/registry/os/index.rs @@ -0,0 +1,57 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use exver::{Version, VersionRange}; +use imbl_value::InternedString; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::rpc_continuations::Guid; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct OsIndex { + pub versions: OsVersionInfoMap, +} + +#[derive(Debug, Default, Deserialize, Serialize, TS)] +pub struct OsVersionInfoMap( + #[ts(as = "BTreeMap::")] pub BTreeMap, +); +impl Map for OsVersionInfoMap { + type Key = Version; + type Value = OsVersionInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(InternedString::from_display(key)) + } + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::from_display(key)) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct OsVersionInfo { + pub headline: String, + pub release_notes: String, + #[ts(type = "string")] + pub source_version: VersionRange, + pub authorized: BTreeSet, + #[ts(as = "BTreeMap::>")] + pub iso: BTreeMap>, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::>")] + pub squashfs: BTreeMap>, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::>")] + pub img: BTreeMap>, // platform (i.e. raspberrypi) -> asset +} + +pub async fn get_os_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().into_os().de() +} diff --git a/core/startos/src/registry/os/mod.rs b/core/startos/src/registry/os/mod.rs new file mode 100644 index 000000000..a1d18cb03 --- /dev/null +++ b/core/startos/src/registry/os/mod.rs @@ -0,0 +1,30 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::context::CliContext; +use crate::util::serde::HandlerExtSerde; + +pub const SIG_CONTEXT: &str = "startos"; + +pub mod asset; +pub mod index; +pub mod version; + +pub fn os_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(index::get_os_index) + .with_display_serializable() + .with_about("List index of OS versions") + .with_call_remote::(), + ) + .subcommand( + "asset", + asset::asset_api::().with_about("Commands to add, sign, or get registry assets"), + ) + .subcommand( + "version", + version::version_api::() + .with_about("Commands to add, remove, or list versions or version signers"), + ) +} diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs new file mode 100644 index 000000000..8e4349ed9 --- /dev/null +++ b/core/startos/src/registry/os/version/mod.rs @@ -0,0 +1,213 @@ +use std::collections::BTreeMap; + +use chrono::Utc; +use clap::Parser; +use exver::{Version, VersionRange}; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use sqlx::query; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::signer::sign::AnyVerifyingKey; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; + +pub mod signer; + +pub fn version_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_version) + .with_metadata("admin", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) + .no_display() + .with_about("Add OS version") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_version) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Remove OS version") + .with_call_remote::(), + ) + .subcommand( + "signer", + signer::signer_api::().with_about("Add, remove, and list version signers"), + ) + .subcommand( + "get", + from_fn_async(get_version) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_version_info(handle.params, result)) + }) + .with_about("Get OS versions and related version info") + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddVersionParams { + #[ts(type = "string")] + pub version: Version, + pub headline: String, + pub release_notes: String, + #[ts(type = "string")] + pub source_version: VersionRange, + #[arg(skip)] + #[ts(skip)] + #[serde(rename = "__auth_signer")] + pub signer: Option, +} + +pub async fn add_version( + ctx: RegistryContext, + AddVersionParams { + version, + headline, + release_notes, + source_version, + signer, + }: AddVersionParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let signer = signer + .map(|s| db.as_index().as_signers().get_signer(&s)) + .transpose()?; + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .upsert(&version, || Ok(OsVersionInfo::default()))? + .mutate(|i| { + i.headline = headline; + i.release_notes = release_notes; + i.source_version = source_version; + i.authorized.extend(signer); + Ok(()) + }) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RemoveVersionParams { + #[ts(type = "string")] + pub version: Version, +} + +pub async fn remove_version( + ctx: RegistryContext, + RemoveVersionParams { version }: RemoveVersionParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .remove(&version)?; + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetOsVersionParams { + #[ts(type = "string | null")] + #[arg(long = "src")] + pub source: Option, + #[ts(type = "string | null")] + #[arg(long = "target")] + pub target: Option, + #[ts(type = "string | null")] + #[arg(long = "id")] + server_id: Option, + #[ts(type = "string | null")] + #[arg(long = "arch")] + arch: Option, +} + +pub async fn get_version( + ctx: RegistryContext, + GetOsVersionParams { + source, + target, + server_id, + arch, + }: GetOsVersionParams, +) -> Result, Error> { + if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { + let created_at = Utc::now(); + + query!( + "INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", + created_at, + server_id, + arch + ) + .execute(pool) + .await?; + } + let target = target.unwrap_or(VersionRange::Any); + ctx.db + .peek() + .await + .into_index() + .into_os() + .into_versions() + .into_entries()? + .into_iter() + .map(|(v, i)| i.de().map(|i| (v, i))) + .filter_ok(|(version, info)| { + version.satisfies(&target) + && source + .as_ref() + .map_or(true, |s| s.satisfies(&info.source_version)) + }) + .collect() +} + +pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, info); + } + + let mut table = Table::new(); + table.add_row(row![bc => + "VERSION", + "HEADLINE", + "RELEASE NOTES", + "ISO PLATFORMS", + "IMG PLATFORMS", + "SQUASHFS PLATFORMS", + ]); + for (version, info) in &info { + table.add_row(row![ + &version.to_string(), + &info.headline, + &info.release_notes, + &info.iso.keys().into_iter().join(", "), + &info.img.keys().into_iter().join(", "), + &info.squashfs.keys().into_iter().join(", "), + ]); + } + table.print_tty(false).unwrap(); +} diff --git a/core/startos/src/registry/os/version/signer.rs b/core/startos/src/registry/os/version/signer.rs new file mode 100644 index 000000000..c72bb5ef4 --- /dev/null +++ b/core/startos/src/registry/os/version/signer.rs @@ -0,0 +1,135 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use exver::Version; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::admin::display_signers; +use crate::registry::context::RegistryContext; +use crate::registry::signer::SignerInfo; +use crate::rpc_continuations::Guid; +use crate::util::serde::HandlerExtSerde; + +pub fn signer_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_version_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Add version signer") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_version_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Remove version signer") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_version_signers) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_about("List version signers and related signer info") + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct VersionSignerParams { + #[ts(type = "string")] + pub version: Version, + pub signer: Guid, +} + +pub async fn add_version_signer( + ctx: RegistryContext, + VersionSignerParams { version, signer }: VersionSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)? + .as_authorized_mut() + .mutate(|s| Ok(s.insert(signer)))?; + + Ok(()) + }) + .await +} + +pub async fn remove_version_signer( + ctx: RegistryContext, + VersionSignerParams { version, signer }: VersionSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if !db + .as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)? + .as_authorized_mut() + .mutate(|s| Ok(s.remove(&signer)))? + { + return Err(Error::new( + eyre!("signer {signer} is not authorized to sign for v{version}"), + ErrorKind::NotFound, + )); + } + + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ListVersionSignersParams { + #[ts(type = "string")] + pub version: Version, +} + +pub async fn list_version_signers( + ctx: RegistryContext, + ListVersionSignersParams { version }: ListVersionSignersParams, +) -> Result, Error> { + let db = ctx.db.peek().await; + db.as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_authorized() + .de()? + .into_iter() + .filter_map(|guid| { + db.as_index() + .as_signers() + .as_idx(&guid) + .map(|s| s.de().map(|s| (guid, s))) + }) + .collect() +} diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs new file mode 100644 index 000000000..c52f06ac0 --- /dev/null +++ b/core/startos/src/registry/package/add.rs @@ -0,0 +1,159 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::HandlerArgs; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, ProgressTrackerWriter}; +use crate::registry::context::RegistryContext; +use crate::registry::package::index::PackageVersionInfo; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::s9pk::S9pk; +use crate::util::io::TrackingIO; + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddPackageParams { + #[ts(type = "string")] + pub url: Url, + #[ts(skip)] + #[serde(rename = "__auth_signer")] + pub uploader: AnyVerifyingKey, + pub commitment: MerkleArchiveCommitment, + pub signature: AnySignature, +} + +pub async fn add_package( + ctx: RegistryContext, + AddPackageParams { + url, + uploader, + commitment, + signature, + }: AddPackageParams, +) -> Result<(), Error> { + uploader + .scheme() + .verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?; + let peek = ctx.db.peek().await; + let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?; + let s9pk = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), + Some(&commitment), + ) + .await?; + + let manifest = s9pk.as_manifest(); + + let mut info = PackageVersionInfo::from_s9pk(&s9pk, url).await?; + if !info.s9pk.signatures.contains_key(&uploader) { + info.s9pk.signatures.insert(uploader.clone(), signature); + } + + ctx.db + .mutate(|db| { + if db.as_admins().de()?.contains(&uploader_guid) + || db + .as_index() + .as_package() + .as_packages() + .as_idx(&manifest.id) + .or_not_found(&manifest.id)? + .as_authorized() + .de()? + .contains(&uploader_guid) + { + let package = db + .as_index_mut() + .as_package_mut() + .as_packages_mut() + .upsert(&manifest.id, || Ok(Default::default()))?; + package.as_versions_mut().insert(&manifest.version, &info)?; + + Ok(()) + } else { + Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) + } + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddPackageParams { + pub file: PathBuf, + pub url: Url, +} + +pub async fn cli_add_package( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliAddPackageParams { file, url }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let s9pk = S9pk::open(&file, None).await?; + + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1)); + let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100)); + let mut index_phase = progress.add_phase( + InternedString::intern("Adding File to Registry Index"), + Some(1), + ); + + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", file.display())); + + sign_phase.start(); + let commitment = s9pk.as_archive().commitment().await?; + let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + sign_phase.complete(); + + verify_phase.start(); + let source = HttpSource::new(ctx.client.clone(), url.clone()).await?; + let len = source.size().await; + let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?; + if let Some(len) = len { + verify_phase.set_total(len); + } + let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase); + src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true) + .await?; + let (_, mut verify_phase) = verify_writer.into_inner(); + verify_phase.complete(); + + index_phase.start(); + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + imbl_value::json!({ + "url": &url, + "signature": AnySignature::Ed25519(signature), + "commitment": commitment, + }), + ) + .await?; + index_phase.complete(); + + progress.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/package/category.rs b/core/startos/src/registry/package/category.rs new file mode 100644 index 000000000..97b0fb227 --- /dev/null +++ b/core/startos/src/registry/package/category.rs @@ -0,0 +1,147 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use imbl_value::InternedString; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::package::index::Category; +use crate::s9pk::manifest::Description; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; + +pub fn category_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_category) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Add a category to the registry") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_category) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Remove a category from the registry") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_categories) + .with_display_serializable() + .with_custom_display_fn(|params, categories| { + Ok(display_categories(params.params, categories)) + }) + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddCategoryParams { + #[ts(type = "string")] + pub id: InternedString, + pub name: String, + #[arg(short, long, help = "Short description for the category")] + pub short: String, + #[arg(short, long, help = "Long description for the category")] + pub long: String, +} + +pub async fn add_category( + ctx: RegistryContext, + AddCategoryParams { + id, + name, + short, + long, + }: AddCategoryParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_index_mut() + .as_package_mut() + .as_categories_mut() + .insert( + &id, + &Category { + name, + description: Description { short, long }, + }, + ) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RemoveCategoryParams { + #[ts(type = "string")] + pub id: InternedString, +} + +pub async fn remove_category( + ctx: RegistryContext, + RemoveCategoryParams { id }: RemoveCategoryParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_index_mut() + .as_package_mut() + .as_categories_mut() + .remove(&id) + }) + .await?; + Ok(()) +} + +pub async fn list_categories( + ctx: RegistryContext, +) -> Result, Error> { + ctx.db + .peek() + .await + .into_index() + .into_package() + .into_categories() + .de() +} + +pub fn display_categories( + params: WithIoFormat, + categories: BTreeMap, +) { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, categories); + } + + let mut table = Table::new(); + table.add_row(row![bc => + "ID", + "NAME", + "SHORT DESCRIPTION", + "LONG DESCRIPTION", + ]); + for (id, info) in categories { + table.add_row(row![ + &*id, + &info.name, + &info.description.short, + &info.description.long, + ]); + } + table.print_tty(false).unwrap(); +} diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs new file mode 100644 index 000000000..cae1289a9 --- /dev/null +++ b/core/startos/src/registry/package/get.rs @@ -0,0 +1,387 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use clap::{Parser, ValueEnum}; +use exver::{ExtendedVersion, VersionRange}; +use imbl_value::InternedString; +use itertools::Itertools; +use models::PackageId; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfo; +use crate::registry::package::index::{PackageIndex, PackageVersionInfo}; +use crate::util::serde::{display_serializable, WithIoFormat}; +use crate::util::VersionString; + +#[derive( + Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum, +)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum PackageDetailLevel { + None, + Short, + Full, +} +impl Default for PackageDetailLevel { + fn default() -> Self { + Self::Short + } +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct PackageInfoShort { + pub release_notes: String, +} + +#[derive(Debug, Deserialize, Serialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[ts(export)] +pub struct GetPackageParams { + pub id: Option, + #[ts(type = "string | null")] + pub version: Option, + pub source_version: Option, + #[ts(skip)] + #[arg(skip)] + #[serde(rename = "__device_info")] + pub device_info: Option, + #[serde(default)] + #[arg(default_value = "none")] + pub other_versions: PackageDetailLevel, +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetPackageResponse { + #[ts(type = "string[]")] + pub categories: BTreeSet, + pub best: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub other_versions: Option>, +} +impl GetPackageResponse { + pub fn tables(&self) -> Vec { + use prettytable::*; + + let mut res = Vec::with_capacity(self.best.len()); + + for (version, info) in &self.best { + let mut table = info.table(version); + + let lesser_versions: BTreeMap<_, _> = self + .other_versions + .as_ref() + .into_iter() + .flatten() + .filter(|(v, _)| ***v < **version) + .collect(); + + if !lesser_versions.is_empty() { + table.add_row(row![bc => "OLDER VERSIONS"]); + table.add_row(row![bc => "VERSION", "RELEASE NOTES"]); + for (version, info) in lesser_versions { + table.add_row(row![AsRef::::as_ref(version), &info.release_notes]); + } + } + + res.push(table); + } + + res + } +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetPackageResponseFull { + #[ts(type = "string[]")] + pub categories: BTreeSet, + pub best: BTreeMap, + pub other_versions: BTreeMap, +} +impl GetPackageResponseFull { + pub fn tables(&self) -> Vec { + let mut res = Vec::with_capacity(self.best.len()); + + let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect(); + + for (version, info) in all { + res.push(info.table(version)); + } + + res + } +} + +pub type GetPackagesResponse = BTreeMap; +pub type GetPackagesResponseFull = BTreeMap; + +fn get_matching_models<'a>( + db: &'a Model, + GetPackageParams { + id, + source_version, + device_info, + .. + }: &GetPackageParams, +) -> Result)>, Error> { + if let Some(id) = id { + if let Some(pkg) = db.as_packages().as_idx(id) { + vec![(id.clone(), pkg)] + } else { + vec![] + } + } else { + db.as_packages().as_entries()? + } + .iter() + .map(|(k, v)| { + Ok(v.as_versions() + .as_entries()? + .into_iter() + .map(|(v, info)| { + Ok::<_, Error>( + if source_version.as_ref().map_or(Ok(true), |source_version| { + Ok::<_, Error>( + source_version.satisfies( + &info + .as_source_version() + .de()? + .unwrap_or(VersionRange::any()), + ), + ) + })? && device_info + .as_ref() + .map_or(Ok(true), |device_info| info.works_for_device(device_info))? + { + Some((k.clone(), ExtendedVersion::from(v), info)) + } else { + None + }, + ) + }) + .flatten_ok()) + }) + .flatten_ok() + .map(|res| res.and_then(|a| a)) + .collect() +} + +pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result { + use patch_db::ModelExt; + + let peek = ctx.db.peek().await; + let mut best: BTreeMap>> = + Default::default(); + let mut other: BTreeMap>> = + Default::default(); + for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { + let package_best = best.entry(id.clone()).or_default(); + let package_other = other.entry(id.clone()).or_default(); + if params + .version + .as_ref() + .map_or(true, |v| version.satisfies(v)) + && package_best.keys().all(|k| !(**k > version)) + { + for worse_version in package_best + .keys() + .filter(|k| ***k < version) + .cloned() + .collect_vec() + { + if let Some(info) = package_best.remove(&worse_version) { + package_other.insert(worse_version, info); + } + } + package_best.insert(version.into(), info); + } else { + package_other.insert(version.into(), info); + } + } + if let Some(id) = params.id { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let best = best + .remove(&id) + .unwrap_or_default() + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?; + let other = other.remove(&id).unwrap_or_default(); + match params.other_versions { + PackageDetailLevel::None => to_value(&GetPackageResponse { + categories, + best, + other_versions: None, + }), + PackageDetailLevel::Short => to_value(&GetPackageResponse { + categories, + best, + other_versions: Some( + other + .into_iter() + .map(|(k, v)| from_value(v.as_value().clone()).map(|v| (k, v))) + .try_collect()?, + ), + }), + PackageDetailLevel::Full => to_value(&GetPackageResponseFull { + categories, + best, + other_versions: other + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + }), + } + } else { + match params.other_versions { + PackageDetailLevel::None => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponse { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: None, + }, + )) + }) + .try_collect::<_, GetPackagesResponse, _>()?, + ), + PackageDetailLevel::Short => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let other = other.remove(&id).unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponse { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: Some( + other + .into_iter() + .map(|(k, v)| { + from_value(v.as_value().clone()).map(|v| (k, v)) + }) + .try_collect()?, + ), + }, + )) + }) + .try_collect::<_, GetPackagesResponse, _>()?, + ), + PackageDetailLevel::Full => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let other = other.remove(&id).unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponseFull { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: other + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + }, + )) + }) + .try_collect::<_, GetPackagesResponseFull, _>()?, + ), + } + } +} + +pub fn display_package_info( + params: WithIoFormat, + info: Value, +) -> Result<(), Error> { + if let Some(format) = params.format { + display_serializable(format, info); + return Ok(()); + } + + if let Some(_) = params.rest.id { + if params.rest.other_versions == PackageDetailLevel::Full { + for table in from_value::(info)?.tables() { + table.print_tty(false)?; + println!(); + } + } else { + for table in from_value::(info)?.tables() { + table.print_tty(false)?; + println!(); + } + } + } else { + if params.rest.other_versions == PackageDetailLevel::Full { + for (_, package) in from_value::(info)? { + for table in package.tables() { + table.print_tty(false)?; + println!(); + } + } + } else { + for (_, package) in from_value::(info)? { + for table in package.tables() { + table.print_tty(false)?; + println!(); + } + } + } + } + Ok(()) +} diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs new file mode 100644 index 000000000..9973bae7e --- /dev/null +++ b/core/startos/src/registry/package/index.rs @@ -0,0 +1,200 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use chrono::Utc; +use exver::{Version, VersionRange}; +use imbl_value::InternedString; +use models::{DataUrl, PackageId, VersionString}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfo; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::rpc_continuations::Guid; +use crate::s9pk::git_hash::GitHash; +use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageIndex { + #[ts(as = "BTreeMap::")] + pub categories: BTreeMap, + pub packages: BTreeMap, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageInfo { + pub authorized: BTreeSet, + pub versions: BTreeMap, + #[ts(type = "string[]")] + pub categories: BTreeSet, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Category { + pub name: String, + pub description: Description, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, + pub description: Option, + pub optional: bool, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageVersionInfo { + #[ts(type = "string")] + pub title: InternedString, + pub icon: DataUrl<'static>, + pub description: Description, + pub release_notes: String, + pub git_hash: GitHash, + #[ts(type = "string")] + pub license: InternedString, + #[ts(type = "string")] + pub wrapper_repo: Url, + #[ts(type = "string")] + pub upstream_repo: Url, + #[ts(type = "string")] + pub support_site: Url, + #[ts(type = "string")] + pub marketing_site: Url, + #[ts(type = "string | null")] + pub donation_url: Option, + pub alerts: Alerts, + pub dependency_metadata: BTreeMap, + #[ts(type = "string")] + pub os_version: Version, + pub hardware_requirements: HardwareRequirements, + #[ts(type = "string | null")] + pub source_version: Option, + pub s9pk: RegistryAsset, +} +impl PackageVersionInfo { + pub async fn from_s9pk(s9pk: &S9pk, url: Url) -> Result { + let manifest = s9pk.as_manifest(); + let mut dependency_metadata = BTreeMap::new(); + for (id, info) in &manifest.dependencies.0 { + let metadata = s9pk.dependency_metadata(id).await?; + dependency_metadata.insert( + id.clone(), + DependencyMetadata { + title: metadata.map(|m| m.title), + icon: s9pk.dependency_icon_data_url(id).await?, + description: info.description.clone(), + optional: info.optional, + }, + ); + } + Ok(Self { + title: manifest.title.clone(), + icon: s9pk.icon_data_url().await?, + description: manifest.description.clone(), + release_notes: manifest.release_notes.clone(), + git_hash: manifest.git_hash.clone().or_not_found("git hash")?, + license: manifest.license.clone(), + wrapper_repo: manifest.wrapper_repo.clone(), + upstream_repo: manifest.upstream_repo.clone(), + support_site: manifest.support_site.clone(), + marketing_site: manifest.marketing_site.clone(), + donation_url: manifest.donation_url.clone(), + alerts: manifest.alerts.clone(), + dependency_metadata, + os_version: manifest.os_version.clone(), + hardware_requirements: manifest.hardware_requirements.clone(), + source_version: None, // TODO + s9pk: RegistryAsset { + published_at: Utc::now(), + url, + commitment: s9pk.as_archive().commitment().await?, + signatures: [( + AnyVerifyingKey::Ed25519(s9pk.as_archive().signer()), + AnySignature::Ed25519(s9pk.as_archive().signature().await?), + )] + .into_iter() + .collect(), + }, + }) + } + pub fn table(&self, version: &VersionString) -> prettytable::Table { + use prettytable::*; + + let mut table = Table::new(); + + table.add_row(row![bc => &self.title]); + table.add_row(row![br -> "VERSION", AsRef::::as_ref(version)]); + table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]); + table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]); + table.add_row(row![ + br -> "DESCRIPTION", + &textwrap::wrap(&self.description.long, 80).join("\n") + ]); + table.add_row(row![br -> "GIT HASH", AsRef::::as_ref(&self.git_hash)]); + table.add_row(row![br -> "LICENSE", &self.license]); + table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); + table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]); + table.add_row(row![br -> "WEBSITE", &self.marketing_site.to_string()]); + table.add_row(row![br -> "SUPPORT", &self.support_site.to_string()]); + + table + } +} +impl Model { + pub fn works_for_device(&self, device_info: &DeviceInfo) -> Result { + if !self.as_os_version().de()?.satisfies(&device_info.os.compat) { + return Ok(false); + } + let hw = self.as_hardware_requirements().de()?; + if let Some(arch) = hw.arch { + if !arch.contains(&device_info.hardware.arch) { + return Ok(false); + } + } + if let Some(ram) = hw.ram { + if device_info.hardware.ram < ram { + return Ok(false); + } + } + for device_filter in hw.device { + if !device_info + .hardware + .devices + .iter() + .filter(|d| d.class() == &*device_filter.class) + .any(|d| device_filter.pattern.as_ref().is_match(d.product())) + { + return Ok(false); + } + } + + Ok(true) + } +} + +pub async fn get_package_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().into_package().de() +} diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs new file mode 100644 index 000000000..74d244deb --- /dev/null +++ b/core/startos/src/registry/package/mod.rs @@ -0,0 +1,54 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::util::serde::HandlerExtSerde; + +pub mod add; +pub mod category; +pub mod get; +pub mod index; +pub mod signer; + +pub fn package_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(index::get_package_index) + .with_display_serializable() + .with_about("List packages and categories") + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add::add_package) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "add", + from_fn_async(add::cli_add_package) + .no_display() + .with_about("Add package to registry index"), + ) + .subcommand( + "signer", + signer::signer_api::().with_about("Add, remove, and list package signers"), + ) + .subcommand( + "get", + from_fn_async(get::get_package) + .with_metadata("get_device_info", Value::Bool(true)) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + get::display_package_info(handle.params, result) + }) + .with_about("List installation candidate package(s)") + .with_call_remote::(), + ) + .subcommand( + "category", + category::category_api::() + .with_about("Update the categories for packages on the registry"), + ) +} diff --git a/core/startos/src/registry/package/signer.rs b/core/startos/src/registry/package/signer.rs new file mode 100644 index 000000000..56bfc9b1c --- /dev/null +++ b/core/startos/src/registry/package/signer.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use models::PackageId; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::admin::display_signers; +use crate::registry::context::RegistryContext; +use crate::registry::signer::SignerInfo; +use crate::rpc_continuations::Guid; +use crate::util::serde::HandlerExtSerde; + +pub fn signer_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_package_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Add package signer") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_package_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_about("Remove package signer") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_package_signers) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_about("List package signers and related signer info") + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct PackageSignerParams { + pub id: PackageId, + pub signer: Guid, +} + +pub async fn add_package_signer( + ctx: RegistryContext, + PackageSignerParams { id, signer }: PackageSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + + db.as_index_mut() + .as_package_mut() + .as_packages_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .as_authorized_mut() + .mutate(|s| Ok(s.insert(signer)))?; + + Ok(()) + }) + .await +} + +pub async fn remove_package_signer( + ctx: RegistryContext, + PackageSignerParams { id, signer }: PackageSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if !db + .as_index_mut() + .as_package_mut() + .as_packages_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .as_authorized_mut() + .mutate(|s| Ok(s.remove(&signer)))? + { + return Err(Error::new( + eyre!("signer {signer} is not authorized to sign for {id}"), + ErrorKind::NotFound, + )); + } + + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ListPackageSignersParams { + pub id: PackageId, +} + +pub async fn list_package_signers( + ctx: RegistryContext, + ListPackageSignersParams { id }: ListPackageSignersParams, +) -> Result, Error> { + let db = ctx.db.peek().await; + db.as_index() + .as_package() + .as_packages() + .as_idx(&id) + .or_not_found(&id)? + .as_authorized() + .de()? + .into_iter() + .filter_map(|guid| { + db.as_index() + .as_signers() + .as_idx(&guid) + .map(|s| s.de().map(|s| (guid, s))) + }) + .collect() +} diff --git a/core/startos/src/registry/signer/commitment/blake3.rs b/core/startos/src/registry/signer/commitment/blake3.rs new file mode 100644 index 000000000..d99e68c16 --- /dev/null +++ b/core/startos/src/registry/signer/commitment/blake3.rs @@ -0,0 +1,50 @@ +use blake3::Hash; +use digest::Update; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::util::serde::Base64; +use crate::CAP_10_MiB; + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Blake3Commitment { + pub hash: Base64<[u8; 32]>, + #[ts(type = "number")] + pub size: u64, +} +impl Digestable for Blake3Commitment { + fn update(&self, digest: &mut D) { + digest.update(&*self.hash); + digest.update(&u64::to_be_bytes(self.size)); + } +} +impl<'a, Resource: ArchiveSource> Commitment<&'a Resource> for Blake3Commitment { + async fn create(resource: &'a Resource) -> Result { + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); + resource.copy_all_to(&mut hasher).await?; + Ok(Self { + size: hasher.position(), + hash: Base64(*hasher.into_inner().finalize().await?.as_bytes()), + }) + } + async fn copy_to( + &self, + resource: &'a Resource, + writer: W, + ) -> Result<(), Error> { + let mut hasher = + VerifyingWriter::new(writer, Some((Hash::from_bytes(*self.hash), self.size))); + resource.copy_to(0, self.size, &mut hasher).await?; + hasher.verify().await?; + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/registry/signer/commitment/merkle_archive.rs new file mode 100644 index 000000000..b27fb7ef4 --- /dev/null +++ b/core/startos/src/registry/signer/commitment/merkle_archive.rs @@ -0,0 +1,127 @@ +use digest::Update; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::MerkleArchive; +use crate::s9pk::S9pk; +use crate::util::io::TrackingIO; +use crate::util::serde::Base64; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct MerkleArchiveCommitment { + pub root_sighash: Base64<[u8; 32]>, + #[ts(type = "number")] + pub root_maxsize: u64, +} +impl MerkleArchiveCommitment { + pub fn from_query(query: &str) -> Result, Error> { + let mut root_sighash = None; + let mut root_maxsize = None; + for (k, v) in form_urlencoded::parse(query.as_bytes()) { + match &*k { + "rootSighash" => { + root_sighash = Some(v.parse()?); + } + "rootMaxsize" => { + root_maxsize = Some(v.parse()?); + } + _ => (), + } + } + if root_sighash.is_some() || root_maxsize.is_some() { + Ok(Some(Self { + root_sighash: root_sighash + .or_not_found("rootSighash required if rootMaxsize specified") + .with_kind(ErrorKind::InvalidRequest)?, + root_maxsize: root_maxsize + .or_not_found("rootMaxsize required if rootSighash specified") + .with_kind(ErrorKind::InvalidRequest)?, + })) + } else { + Ok(None) + } + } +} +impl Digestable for MerkleArchiveCommitment { + fn update(&self, digest: &mut D) { + digest.update(&*self.root_sighash); + digest.update(&u64::to_be_bytes(self.root_maxsize)); + } +} +impl<'a, S: FileSource + Clone> Commitment<&'a MerkleArchive> for MerkleArchiveCommitment { + async fn create(resource: &'a MerkleArchive) -> Result { + resource.commitment().await + } + async fn check(&self, resource: &'a MerkleArchive) -> Result<(), Error> { + let MerkleArchiveCommitment { + root_sighash, + root_maxsize, + } = resource.commitment().await?; + if root_sighash != self.root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if root_maxsize > self.root_maxsize { + return Err(Error::new( + eyre!("merkle root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + Ok(()) + } + async fn copy_to( + &self, + resource: &'a MerkleArchive, + writer: W, + ) -> Result<(), Error> { + self.check(resource).await?; + resource + .serialize(&mut TrackingIO::new(0, writer), true) + .await + } +} + +impl<'a, S: FileSource + Clone> Commitment<&'a S9pk> for MerkleArchiveCommitment { + async fn create(resource: &'a S9pk) -> Result { + resource.as_archive().commitment().await + } + async fn check(&self, resource: &'a S9pk) -> Result<(), Error> { + let MerkleArchiveCommitment { + root_sighash, + root_maxsize, + } = resource.as_archive().commitment().await?; + if root_sighash != self.root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if root_maxsize > self.root_maxsize { + return Err(Error::new( + eyre!("merkle root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + Ok(()) + } + async fn copy_to( + &self, + resource: &'a S9pk, + writer: W, + ) -> Result<(), Error> { + self.check(resource).await?; + resource + .clone() + .serialize(&mut TrackingIO::new(0, writer), true) + .await + } +} diff --git a/core/startos/src/registry/signer/commitment/mod.rs b/core/startos/src/registry/signer/commitment/mod.rs new file mode 100644 index 000000000..b85e02a4e --- /dev/null +++ b/core/startos/src/registry/signer/commitment/mod.rs @@ -0,0 +1,25 @@ +use digest::Update; +use futures::Future; +use tokio::io::AsyncWrite; + +use crate::prelude::*; + +pub mod blake3; +pub mod merkle_archive; +pub mod request; + +pub trait Digestable { + fn update(&self, digest: &mut D); +} + +pub trait Commitment: Sized + Digestable { + fn create(resource: Resource) -> impl Future> + Send; + fn copy_to( + &self, + resource: Resource, + writer: W, + ) -> impl Future> + Send; + fn check(&self, resource: Resource) -> impl Future> + Send { + self.copy_to(resource, tokio::io::sink()) + } +} diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs new file mode 100644 index 000000000..e5bb776bf --- /dev/null +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -0,0 +1,103 @@ +use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::body::Body; +use axum::extract::Request; +use digest::Update; +use futures::TryStreamExt; +use http::HeaderValue; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use tokio_util::io::StreamReader; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::util::serde::Base64; + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct RequestCommitment { + #[ts(type = "number")] + pub timestamp: i64, + #[ts(type = "number")] + pub nonce: u64, + #[ts(type = "number")] + pub size: u64, + pub blake3: Base64<[u8; 32]>, +} +impl RequestCommitment { + pub fn append_query(&self, url: &mut Url) { + url.query_pairs_mut() + .append_pair("timestamp", &self.timestamp.to_string()) + .append_pair("nonce", &self.nonce.to_string()) + .append_pair("size", &self.size.to_string()) + .append_pair("blake3", &self.blake3.to_string()); + } + pub fn from_query(query: &HeaderValue) -> Result { + let query: BTreeMap<_, _> = form_urlencoded::parse(query.as_bytes()).collect(); + Ok(Self { + timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?, + nonce: query.get("nonce").or_not_found("nonce")?.parse()?, + size: query.get("size").or_not_found("size")?.parse()?, + blake3: query.get("blake3").or_not_found("blake3")?.parse()?, + }) + } +} +impl Digestable for RequestCommitment { + fn update(&self, digest: &mut D) { + digest.update(&i64::to_be_bytes(self.timestamp)); + digest.update(&u64::to_be_bytes(self.nonce)); + digest.update(&u64::to_be_bytes(self.size)); + digest.update(&*self.blake3); + } +} +impl<'a> Commitment<&'a mut Request> for RequestCommitment { + async fn create(resource: &'a mut Request) -> Result { + use http_body_util::BodyExt; + + let body = std::mem::replace(resource.body_mut(), Body::empty()) + .collect() + .await + .with_kind(ErrorKind::Network)? + .to_bytes(); + let res = Self { + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1), + nonce: rand::random(), + size: body.len() as u64, + blake3: Base64(*blake3::hash(&*body).as_bytes()), + }; + *resource.body_mut() = Body::from(body); + Ok(res) + } + async fn copy_to( + &self, + resource: &'a mut Request, + writer: W, + ) -> Result<(), Error> { + use tokio::io::AsyncReadExt; + + let mut body = StreamReader::new( + std::mem::replace(resource.body_mut(), Body::empty()) + .into_data_stream() + .map_err(std::io::Error::other), + ) + .take(self.size); + + let mut writer = VerifyingWriter::new( + writer, + Some((blake3::Hash::from_bytes(*self.blake3), self.size)), + ); + tokio::io::copy(&mut body, &mut writer).await?; + writer.verify().await?; + + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/mod.rs b/core/startos/src/registry/signer/mod.rs new file mode 100644 index 000000000..137c40f0f --- /dev/null +++ b/core/startos/src/registry/signer/mod.rs @@ -0,0 +1,154 @@ +use std::collections::HashSet; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use itertools::Itertools; +use models::FromStrParser; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::commitment::Digestable; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; + +pub mod commitment; +pub mod sign; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct SignerInfo { + pub name: String, + pub contact: Vec, + pub keys: HashSet, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +// TODO: better types +pub enum ContactInfo { + Email(String), + Matrix(String), + Website(#[ts(type = "string")] Url), +} +impl std::fmt::Display for ContactInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Email(e) => write!(f, "mailto:{e}"), + Self::Matrix(m) => write!(f, "https://matrix.to/#/{m}"), + Self::Website(w) => write!(f, "{w}"), + } + } +} +impl FromStr for ContactInfo { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(if let Some(s) = s.strip_prefix("mailto:") { + Self::Email(s.to_owned()) + } else if let Some(s) = s.strip_prefix("https://matrix.to/#/") { + Self::Matrix(s.to_owned()) + } else { + Self::Website(s.parse()?) + }) + } +} +impl ValueParserFactory for ContactInfo { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum AcceptSigners { + #[serde(skip)] + Accepted, + Signer(AnyVerifyingKey), + Any(Vec), + All(Vec), +} +impl AcceptSigners { + const fn null() -> Self { + Self::Any(Vec::new()) + } + pub fn flatten(self) -> Self { + match self { + Self::Any(mut s) | Self::All(mut s) if s.len() == 1 => s.swap_remove(0).flatten(), + s => s, + } + } + pub fn accepted(&self) -> bool { + match self { + Self::Accepted => true, + _ => false, + } + } + pub fn try_accept(self) -> Result<(), Error> { + if self.accepted() { + Ok(()) + } else { + Err(Error::new( + eyre!("signer(s) not accepted"), + ErrorKind::InvalidSignature, + )) + } + } + pub fn process_signature( + &mut self, + signer: &AnyVerifyingKey, + commitment: &impl Digestable, + context: &str, + signature: &AnySignature, + ) -> Result<(), Error> { + let mut res = Ok(()); + let new = match std::mem::replace(self, Self::null()) { + Self::Accepted => Self::Accepted, + Self::Signer(s) => { + if &s == signer { + res = signer + .scheme() + .verify_commitment(signer, commitment, context, signature); + Self::Accepted + } else { + Self::Signer(s) + } + } + Self::All(mut s) => { + res = s + .iter_mut() + .map(|s| s.process_signature(signer, commitment, context, signature)) + .collect(); + if s.iter().all(|s| s.accepted()) { + Self::Accepted + } else { + Self::All(s) + } + } + Self::Any(mut s) => { + match s + .iter_mut() + .map(|s| { + s.process_signature(signer, commitment, context, signature)?; + Ok(s) + }) + .filter_ok(|s| s.accepted()) + .next() + { + Some(Ok(s)) => std::mem::replace(s, Self::null()), + Some(Err(e)) => { + res = Err(e); + Self::Any(s) + } + None => Self::Any(s), + } + } + }; + *self = new; + res + } +} diff --git a/core/startos/src/registry/signer/sign/ed25519.rs b/core/startos/src/registry/signer/sign/ed25519.rs new file mode 100644 index 000000000..3ec4c136e --- /dev/null +++ b/core/startos/src/registry/signer/sign/ed25519.rs @@ -0,0 +1,34 @@ +use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; +use sha2::Sha512; + +use crate::prelude::*; +use crate::registry::signer::sign::SignatureScheme; + +pub struct Ed25519; +impl SignatureScheme for Ed25519 { + type SigningKey = SigningKey; + type VerifyingKey = VerifyingKey; + type Signature = Signature; + type Digest = Sha512; + fn new_digest(&self) -> Self::Digest { + ::new() + } + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result { + Ok(key.sign_prehashed(digest, Some(context.as_bytes()))?) + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + key.verify_prehashed_strict(digest, Some(context.as_bytes()), signature)?; + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/sign/mod.rs b/core/startos/src/registry/signer/sign/mod.rs new file mode 100644 index 000000000..6a95a2490 --- /dev/null +++ b/core/startos/src/registry/signer/sign/mod.rs @@ -0,0 +1,347 @@ +use std::fmt::Display; +use std::str::FromStr; + +use ::ed25519::pkcs8::BitStringRef; +use clap::builder::ValueParserFactory; +use der::referenced::OwnedToRef; +use models::FromStrParser; +use pkcs8::der::AnyRef; +use pkcs8::{PrivateKeyInfo, SubjectPublicKeyInfo}; +use serde::{Deserialize, Serialize}; +use sha2::Sha512; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::Digestable; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::util::serde::{deserialize_from_str, serialize_display}; + +pub mod ed25519; + +pub trait SignatureScheme { + type SigningKey; + type VerifyingKey; + type Signature; + type Digest: digest::Update; + fn new_digest(&self) -> Self::Digest; + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result; + fn sign_commitment( + &self, + key: &Self::SigningKey, + commitment: &C, + context: &str, + ) -> Result { + let mut digest = self.new_digest(); + commitment.update(&mut digest); + self.sign(key, digest, context) + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error>; + fn verify_commitment( + &self, + key: &Self::VerifyingKey, + commitment: &C, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + let mut digest = self.new_digest(); + commitment.update(&mut digest); + self.verify(key, digest, context, signature) + } +} + +pub enum AnyScheme { + Ed25519(Ed25519), +} +impl From for AnyScheme { + fn from(value: Ed25519) -> Self { + Self::Ed25519(value) + } +} +impl SignatureScheme for AnyScheme { + type SigningKey = AnySigningKey; + type VerifyingKey = AnyVerifyingKey; + type Signature = AnySignature; + type Digest = AnyDigest; + fn new_digest(&self) -> Self::Digest { + match self { + Self::Ed25519(s) => AnyDigest::Sha512(s.new_digest()), + } + } + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result { + match (self, key, digest) { + (Self::Ed25519(s), AnySigningKey::Ed25519(key), AnyDigest::Sha512(digest)) => { + Ok(AnySignature::Ed25519(s.sign(key, digest, context)?)) + } + _ => Err(Error::new( + eyre!("mismatched signature algorithm"), + ErrorKind::InvalidSignature, + )), + } + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + match (self, key, digest, signature) { + ( + Self::Ed25519(s), + AnyVerifyingKey::Ed25519(key), + AnyDigest::Sha512(digest), + AnySignature::Ed25519(signature), + ) => s.verify(key, digest, context, signature), + _ => Err(Error::new( + eyre!("mismatched signature algorithm"), + ErrorKind::InvalidSignature, + )), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, TS)] +#[ts(export, type = "string")] +pub enum AnySigningKey { + Ed25519(::SigningKey), +} +impl AnySigningKey { + pub fn scheme(&self) -> AnyScheme { + match self { + Self::Ed25519(_) => AnyScheme::Ed25519(Ed25519), + } + } + pub fn verifying_key(&self) -> AnyVerifyingKey { + match self { + Self::Ed25519(k) => AnyVerifyingKey::Ed25519(k.into()), + } + } +} +impl<'a> TryFrom> for AnySigningKey { + type Error = pkcs8::Error; + fn try_from(value: PrivateKeyInfo<'a>) -> Result { + if value.algorithm == ed25519_dalek::pkcs8::ALGORITHM_ID { + Ok(Self::Ed25519(ed25519_dalek::SigningKey::try_from(value)?)) + } else { + Err(pkcs8::spki::Error::OidUnknown { + oid: value.algorithm.oid, + } + .into()) + } + } +} +impl pkcs8::EncodePrivateKey for AnySigningKey { + fn to_pkcs8_der(&self) -> pkcs8::Result { + match self { + Self::Ed25519(s) => s.to_pkcs8_der(), + } + } +} +impl FromStr for AnySigningKey { + type Err = Error; + fn from_str(s: &str) -> Result { + use pkcs8::DecodePrivateKey; + Self::from_pkcs8_pem(s).with_kind(ErrorKind::Deserialization) + } +} +impl Display for AnySigningKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use pkcs8::EncodePrivateKey; + f.write_str( + &self + .to_pkcs8_pem(pkcs8::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnySigningKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnySigningKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, TS)] +#[ts(export, type = "string")] +pub enum AnyVerifyingKey { + Ed25519(::VerifyingKey), +} +impl AnyVerifyingKey { + pub fn scheme(&self) -> AnyScheme { + match self { + Self::Ed25519(_) => AnyScheme::Ed25519(Ed25519), + } + } +} +impl<'a> TryFrom, BitStringRef<'a>>> for AnyVerifyingKey { + type Error = pkcs8::spki::Error; + fn try_from( + value: SubjectPublicKeyInfo, BitStringRef<'a>>, + ) -> Result { + if value.algorithm == ed25519_dalek::pkcs8::ALGORITHM_ID { + Ok(Self::Ed25519(ed25519_dalek::VerifyingKey::try_from(value)?)) + } else { + Err(pkcs8::spki::Error::OidUnknown { + oid: value.algorithm.oid, + }) + } + } +} +impl pkcs8::EncodePublicKey for AnyVerifyingKey { + fn to_public_key_der(&self) -> pkcs8::spki::Result { + match self { + Self::Ed25519(s) => s.to_public_key_der(), + } + } +} +impl FromStr for AnyVerifyingKey { + type Err = Error; + fn from_str(s: &str) -> Result { + use pkcs8::DecodePublicKey; + Self::from_public_key_pem(s).with_kind(ErrorKind::Deserialization) + } +} +impl Display for AnyVerifyingKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use pkcs8::EncodePublicKey; + f.write_str( + &self + .to_public_key_pem(pkcs8::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnyVerifyingKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnyVerifyingKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} +impl ValueParserFactory for AnyVerifyingKey { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Clone, Debug)] +pub enum AnyDigest { + Sha512(Sha512), +} +impl digest::Update for AnyDigest { + fn update(&mut self, data: &[u8]) { + match self { + Self::Sha512(d) => digest::Update::update(d, data), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, TS)] +#[ts(export, type = "string")] +pub enum AnySignature { + Ed25519(::Signature), +} +impl FromStr for AnySignature { + type Err = Error; + fn from_str(s: &str) -> Result { + use der::DecodePem; + + #[derive(der::Sequence)] + struct AnySignatureDer { + alg: pkcs8::spki::AlgorithmIdentifierOwned, + sig: der::asn1::OctetString, + } + impl der::pem::PemLabel for AnySignatureDer { + const PEM_LABEL: &'static str = "SIGNATURE"; + } + + let der = AnySignatureDer::from_pem(s.as_bytes()).with_kind(ErrorKind::Deserialization)?; + if der.alg.oid == ed25519_dalek::pkcs8::ALGORITHM_ID.oid + && der.alg.parameters.owned_to_ref() == ed25519_dalek::pkcs8::ALGORITHM_ID.parameters + { + Ok(Self::Ed25519( + ed25519_dalek::Signature::from_slice(der.sig.as_bytes()) + .with_kind(ErrorKind::Deserialization)?, + )) + } else { + Err(pkcs8::spki::Error::OidUnknown { oid: der.alg.oid }) + .with_kind(ErrorKind::Deserialization) + } + } +} +impl Display for AnySignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use der::EncodePem; + + #[derive(der::Sequence)] + struct AnySignatureDer<'a> { + alg: pkcs8::AlgorithmIdentifierRef<'a>, + sig: der::asn1::OctetString, + } + impl<'a> der::pem::PemLabel for AnySignatureDer<'a> { + const PEM_LABEL: &'static str = "SIGNATURE"; + } + f.write_str( + &match self { + Self::Ed25519(s) => AnySignatureDer { + alg: ed25519_dalek::pkcs8::ALGORITHM_ID, + sig: der::asn1::OctetString::new(s.to_bytes()).map_err(|_| std::fmt::Error)?, + }, + } + .to_pem(der::pem::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnySignature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnySignature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} diff --git a/core/startos/src/rpc_continuations.rs b/core/startos/src/rpc_continuations.rs new file mode 100644 index 000000000..4614f8fa6 --- /dev/null +++ b/core/startos/src/rpc_continuations.rs @@ -0,0 +1,259 @@ +use std::collections::BTreeMap; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Mutex as SyncMutex; +use std::task::{Context, Poll}; +use std::time::Duration; + +use axum::extract::ws::WebSocket; +use axum::extract::Request; +use axum::response::Response; +use clap::builder::ValueParserFactory; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use helpers::TimedResource; +use imbl_value::InternedString; +use models::FromStrParser; +use tokio::sync::{broadcast, Mutex as AsyncMutex}; +use ts_rs::TS; + +#[allow(unused_imports)] +use crate::prelude::*; +use crate::util::new_guid; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, TS, +)] +#[ts(type = "string")] +pub struct Guid(InternedString); +impl Guid { + pub fn new() -> Self { + Self(new_guid()) + } + + pub fn from(r: &str) -> Option { + if r.len() != 32 { + return None; + } + for c in r.chars() { + if !(c >= 'A' && c <= 'Z' || c >= '2' && c <= '7') { + return None; + } + } + Some(Guid(InternedString::intern(r))) + } +} +impl Default for Guid { + fn default() -> Self { + Self::new() + } +} +impl AsRef for Guid { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl FromStr for Guid { + type Err = Error; + fn from_str(s: &str) -> Result { + Self::from(s).ok_or_else(|| Error::new(eyre!("invalid guid"), ErrorKind::Deserialization)) + } +} +impl ValueParserFactory for Guid { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[test] +fn parse_guid() { + println!("{:?}", Guid::from(&format!("{}", Guid::new()))) +} + +impl std::fmt::Display for Guid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +pub struct RestFuture { + kill: Option>, + fut: BoxFuture<'static, Result>, +} +impl Future for RestFuture { + type Output = Result; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.kill.as_ref().map_or(false, |k| !k.is_empty()) { + Poll::Ready(Err(Error::new( + eyre!("session killed"), + ErrorKind::Authorization, + ))) + } else { + self.fut.poll_unpin(cx) + } + } +} +pub type RestHandler = Box RestFuture + Send>; + +pub struct WebSocketFuture { + kill: Option>, + fut: BoxFuture<'static, ()>, +} +impl Future for WebSocketFuture { + type Output = (); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.kill.as_ref().map_or(false, |k| !k.is_empty()) { + Poll::Ready(()) + } else { + self.fut.poll_unpin(cx) + } + } +} +pub type WebSocketHandler = Box WebSocketFuture + Send>; + +pub enum RpcContinuation { + Rest(TimedResource), + WebSocket(TimedResource), +} +impl RpcContinuation { + pub fn rest(handler: F, timeout: Duration) -> Self + where + F: FnOnce(Request) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, + { + RpcContinuation::Rest(TimedResource::new( + Box::new(|req| RestFuture { + kill: None, + fut: handler(req).boxed(), + }), + timeout, + )) + } + pub fn ws(handler: F, timeout: Duration) -> Self + where + F: FnOnce(WebSocket) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + RpcContinuation::WebSocket(TimedResource::new( + Box::new(|ws| WebSocketFuture { + kill: None, + fut: handler(ws).boxed(), + }), + timeout, + )) + } + pub fn rest_authed(ctx: Ctx, session: T, handler: F, timeout: Duration) -> Self + where + Ctx: AsRef>, + T: Eq + Ord, + F: FnOnce(Request) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, + { + let kill = Some(ctx.as_ref().subscribe_to_kill(session)); + RpcContinuation::Rest(TimedResource::new( + Box::new(|req| RestFuture { + kill, + fut: handler(req).boxed(), + }), + timeout, + )) + } + pub fn ws_authed(ctx: Ctx, session: T, handler: F, timeout: Duration) -> Self + where + Ctx: AsRef>, + T: Eq + Ord, + F: FnOnce(WebSocket) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + let kill = Some(ctx.as_ref().subscribe_to_kill(session)); + RpcContinuation::WebSocket(TimedResource::new( + Box::new(|ws| WebSocketFuture { + kill, + fut: handler(ws).boxed(), + }), + timeout, + )) + } + pub fn is_timed_out(&self) -> bool { + match self { + RpcContinuation::Rest(a) => a.is_timed_out(), + RpcContinuation::WebSocket(a) => a.is_timed_out(), + } + } +} + +pub struct RpcContinuations(AsyncMutex>); +impl RpcContinuations { + pub fn new() -> Self { + RpcContinuations(AsyncMutex::new(BTreeMap::new())) + } + + #[instrument(skip_all)] + pub async fn clean(&self) { + let mut continuations = self.0.lock().await; + let mut to_remove = Vec::new(); + for (guid, cont) in &*continuations { + if cont.is_timed_out() { + to_remove.push(guid.clone()); + } + } + for guid in to_remove { + continuations.remove(&guid); + } + } + + #[instrument(skip_all)] + pub async fn add(&self, guid: Guid, handler: RpcContinuation) { + self.clean().await; + self.0.lock().await.insert(guid, handler); + } + + pub async fn get_ws_handler(&self, guid: &Guid) -> Option { + let mut continuations = self.0.lock().await; + if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { + return None; + } + let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else { + return None; + }; + x.get().await + } + + pub async fn get_rest_handler(&self, guid: &Guid) -> Option { + let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap> = + self.0.lock().await; + if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { + return None; + } + let Some(RpcContinuation::Rest(x)) = continuations.remove(guid) else { + return None; + }; + x.get().await + } +} + +pub struct OpenAuthedContinuations(SyncMutex>>); +impl OpenAuthedContinuations +where + T: Eq + Ord, +{ + pub fn new() -> Self { + Self(SyncMutex::new(BTreeMap::new())) + } + pub fn kill(&self, session: &T) { + if let Some(channel) = self.0.lock().unwrap().remove(session) { + channel.send(()).ok(); + } + } + fn subscribe_to_kill(&self, session: T) -> broadcast::Receiver<()> { + let mut map = self.0.lock().unwrap(); + if let Some(send) = map.get(&session) { + send.subscribe() + } else { + let (send, recv) = broadcast::channel(1); + map.insert(session, send); + recv + } + } +} diff --git a/core/startos/src/s9pk/git_hash.rs b/core/startos/src/s9pk/git_hash.rs index b2990a111..762ef8704 100644 --- a/core/startos/src/s9pk/git_hash.rs +++ b/core/startos/src/s9pk/git_hash.rs @@ -1,24 +1,62 @@ use std::path::Path; -use crate::Error; +use tokio::process::Command; +use ts_rs::TS; -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +use crate::prelude::*; +use crate::util::Invoke; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)] +#[ts(type = "string")] pub struct GitHash(String); impl GitHash { pub async fn from_path(path: impl AsRef) -> Result { - let hash = tokio::process::Command::new("git") - .args(["describe", "--always", "--abbrev=40", "--dirty=-modified"]) - .current_dir(path) + let mut hash = String::from_utf8( + Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .current_dir(&path) + .invoke(ErrorKind::Git) + .await?, + )?; + if Command::new("git") + .arg("diff-index") + .arg("--quiet") + .arg("HEAD") + .arg("--") + .invoke(ErrorKind::Git) + .await + .is_err() + { + hash += "-modified"; + } + Ok(GitHash(hash)) + } + pub fn load_sync() -> Option { + let mut hash = String::from_utf8( + std::process::Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .output() + .ok()? + .stdout, + ) + .ok()?; + if !std::process::Command::new("git") + .arg("diff-index") + .arg("--quiet") + .arg("HEAD") + .arg("--") .output() - .await?; - if !hash.status.success() { - return Err(Error::new( - color_eyre::eyre::eyre!("Could not get hash: {}", String::from_utf8(hash.stderr)?), - crate::ErrorKind::Filesystem, - )); + .ok()? + .status + .success() + { + hash += "-modified"; } - Ok(GitHash(String::from_utf8(hash.stdout)?)) + + Some(GitHash(hash)) } } diff --git a/core/startos/src/s9pk/merkle_archive/directory_contents.rs b/core/startos/src/s9pk/merkle_archive/directory_contents.rs new file mode 100644 index 000000000..b39789222 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/directory_contents.rs @@ -0,0 +1,323 @@ +use std::ffi::OsStr; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use blake3::Hash; +use futures::future::BoxFuture; +use futures::FutureExt; +use imbl::OrdMap; +use imbl_value::InternedString; +use itertools::Itertools; +use tokio::io::AsyncRead; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::sink::Sink; +use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; +use crate::s9pk::merkle_archive::write_queue::WriteQueue; +use crate::s9pk::merkle_archive::{varint, Entry, EntryContents}; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::CAP_10_MiB; + +#[derive(Clone)] +pub struct DirectoryContents { + contents: OrdMap>, + /// used to optimize files to have earliest needed information up front + sort_by: Option std::cmp::Ordering + Send + Sync>>, +} +impl Debug for DirectoryContents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DirectoryContents") + .field("contents", &self.contents) + .finish_non_exhaustive() + } +} +impl DirectoryContents { + pub fn new() -> Self { + Self { + contents: OrdMap::new(), + sort_by: None, + } + } + + pub fn sort_by( + &mut self, + sort_by: impl Fn(&str, &str) -> std::cmp::Ordering + Send + Sync + 'static, + ) { + self.sort_by = Some(Arc::new(sort_by)) + } + + #[instrument(skip_all)] + pub fn get_path(&self, path: impl AsRef) -> Option<&Entry> { + let mut dir = Some(self); + let mut res = None; + for segment in path.as_ref().into_iter() { + let segment = segment.to_str()?; + if segment == "/" { + continue; + } + res = dir?.get(segment); + if let Some(EntryContents::Directory(d)) = res.as_ref().map(|e| e.as_contents()) { + dir = Some(d); + } else { + dir = None + } + } + res + } + + pub fn file_paths(&self, prefix: impl AsRef) -> Vec { + let prefix = prefix.as_ref(); + let mut res = Vec::new(); + for (name, entry) in &self.contents { + let path = prefix.join(name); + if let EntryContents::Directory(d) = entry.as_contents() { + res.push(path.join("")); + res.append(&mut d.file_paths(path)); + } else { + res.push(path); + } + } + res + } + + pub const fn header_size() -> u64 { + 8 // position: u64 BE + + 8 // size: u64 BE + } + + #[instrument(skip_all)] + pub async fn serialize_header(&self, position: u64, w: &mut W) -> Result { + use tokio::io::AsyncWriteExt; + + let size = self.toc_size(); + + w.write_all(&position.to_be_bytes()).await?; + w.write_all(&size.to_be_bytes()).await?; + + Ok(position) + } + + pub fn toc_size(&self) -> u64 { + self.iter().fold( + varint::serialized_varint_size(self.len() as u64), + |acc, (name, entry)| { + acc + varint::serialized_varstring_size(&**name) + entry.header_size() + }, + ) + } +} +impl DirectoryContents { + pub fn with_stem(&self, stem: &str) -> impl Iterator)> { + let prefix = InternedString::intern(stem); + let (_, center, right) = self.split_lookup(&*stem); + center.map(|e| (prefix.clone(), e)).into_iter().chain( + right.into_iter().take_while(move |(k, _)| { + Path::new(&**k).file_stem() == Some(OsStr::new(&*prefix)) + }), + ) + } + pub fn insert_path(&mut self, path: impl AsRef, entry: Entry) -> Result<(), Error> { + let path = path.as_ref(); + let (parent, Some(file)) = (path.parent(), path.file_name().and_then(|f| f.to_str())) + else { + return Err(Error::new( + eyre!("cannot create file at root"), + ErrorKind::Pack, + )); + }; + let mut dir = self; + for segment in parent.into_iter().flatten() { + let segment = segment + .to_str() + .ok_or_else(|| Error::new(eyre!("non-utf8 path segment"), ErrorKind::Utf8))?; + if segment == "/" { + continue; + } + if !dir.contains_key(segment) { + dir.insert( + segment.into(), + Entry::new(EntryContents::Directory(DirectoryContents::new())), + ); + } + if let Some(EntryContents::Directory(d)) = + dir.get_mut(segment).map(|e| e.as_contents_mut()) + { + dir = d; + } else { + return Err(Error::new(eyre!("failed to insert entry at path {path:?}: ancestor exists and is not a directory"), ErrorKind::Pack)); + } + } + dir.insert(file.into(), entry); + Ok(()) + } +} +impl DirectoryContents> { + #[instrument(skip_all)] + pub fn deserialize<'a>( + source: &'a S, + header: &'a mut (impl AsyncRead + Unpin + Send), + (sighash, max_size): (Hash, u64), + ) -> BoxFuture<'a, Result> { + async move { + use tokio::io::AsyncReadExt; + + let mut position = [0u8; 8]; + header.read_exact(&mut position).await?; + let position = u64::from_be_bytes(position); + + let mut size = [0u8; 8]; + header.read_exact(&mut size).await?; + let size = u64::from_be_bytes(size); + + ensure_code!( + size <= max_size, + ErrorKind::InvalidSignature, + "size is greater than signed" + ); + + let mut toc_reader = source.fetch(position, size).await?; + + let len = varint::deserialize_varint(&mut toc_reader).await?; + let mut entries = OrdMap::new(); + for _ in 0..len { + let name = varint::deserialize_varstring(&mut toc_reader).await?; + let entry = Entry::deserialize(source.clone(), &mut toc_reader).await?; + entries.insert(name.into(), entry); + } + + let res = Self { + contents: entries, + sort_by: None, + }; + + if res.sighash().await? == sighash { + Ok(res) + } else { + Err(Error::new( + eyre!("hash sum does not match"), + ErrorKind::InvalidSignature, + )) + } + } + .boxed() + } +} +impl DirectoryContents { + pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { + for k in self.keys().cloned().collect::>() { + let path = Path::new(&*k); + if let Some(v) = self.get_mut(&k) { + if !filter(path) { + if v.hash.is_none() { + return Err(Error::new( + eyre!( + "cannot filter out unhashed file {}, run `update_hashes` first", + path.display() + ), + ErrorKind::InvalidRequest, + )); + } + v.contents = EntryContents::Missing; + } else { + let filter: Box bool> = Box::new(|p| filter(&path.join(p))); + v.filter(filter)?; + } + } + } + Ok(()) + } + #[instrument(skip_all)] + pub fn update_hashes<'a>(&'a mut self, only_missing: bool) -> BoxFuture<'a, Result<(), Error>> { + async move { + for key in self.keys().cloned().collect::>() { + if let Some(entry) = self.get_mut(&key) { + entry.update_hash(only_missing).await?; + } + } + Ok(()) + } + .boxed() + } + + #[instrument(skip_all)] + pub fn sighash<'a>(&'a self) -> BoxFuture<'a, Result> { + async move { + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); + let mut sig_contents = OrdMap::new(); + for (name, entry) in &**self { + sig_contents.insert(name.clone(), entry.to_missing().await?); + } + Self { + contents: sig_contents, + sort_by: None, + } + .serialize_toc(&mut WriteQueue::new(0), &mut hasher) + .await?; + let hash = hasher.into_inner().finalize().await?; + Ok(hash) + } + .boxed() + } + + #[instrument(skip_all)] + pub async fn serialize_toc<'a, W: Sink>( + &'a self, + queue: &mut WriteQueue<'a, S>, + w: &mut W, + ) -> Result<(), Error> { + varint::serialize_varint(self.len() as u64, w).await?; + for (name, entry) in self.iter().sorted_by(|a, b| match (a, b, &self.sort_by) { + ((_, a), (_, b), _) if a.as_contents().is_dir() && !b.as_contents().is_dir() => { + std::cmp::Ordering::Less + } + ((_, a), (_, b), _) if !a.as_contents().is_dir() && b.as_contents().is_dir() => { + std::cmp::Ordering::Greater + } + ((_, a), (_, b), _) + if a.as_contents().is_missing() && !b.as_contents().is_missing() => + { + std::cmp::Ordering::Greater + } + ((_, a), (_, b), _) + if !a.as_contents().is_missing() && b.as_contents().is_missing() => + { + std::cmp::Ordering::Less + } + ((n_a, a), (n_b, b), _) + if a.as_contents().is_missing() && b.as_contents().is_missing() => + { + n_a.cmp(n_b) + } + ((a, _), (b, _), Some(sort_by)) => sort_by(&***a, &***b), + _ => std::cmp::Ordering::Equal, + }) { + varint::serialize_varstring(&**name, w).await?; + entry.serialize_header(queue.add(entry).await?, w).await?; + } + + Ok(()) + } + + pub fn into_dyn(self) -> DirectoryContents { + DirectoryContents { + contents: self + .contents + .into_iter() + .map(|(k, v)| (k, v.into_dyn())) + .collect(), + sort_by: self.sort_by, + } + } +} +impl std::ops::Deref for DirectoryContents { + type Target = OrdMap>; + fn deref(&self) -> &Self::Target { + &self.contents + } +} +impl std::ops::DerefMut for DirectoryContents { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.contents + } +} diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs new file mode 100644 index 000000000..c9a2fd31b --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -0,0 +1,105 @@ +use std::ffi::OsStr; +use std::path::Path; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::Entry; + +/// An object for tracking the files expected to be in an s9pk +pub struct Expected<'a, T> { + keep: DirectoryContents<()>, + dir: &'a DirectoryContents, +} +impl<'a, T> Expected<'a, T> { + pub fn new(dir: &'a DirectoryContents) -> Self { + Self { + keep: DirectoryContents::new(), + dir, + } + } +} +impl<'a, T: Clone> Expected<'a, T> { + pub fn check_file(&mut self, path: impl AsRef) -> Result<(), Error> { + if self + .dir + .get_path(path.as_ref()) + .and_then(|e| e.as_file()) + .is_some() + { + self.keep.insert_path(path, Entry::file(()))?; + Ok(()) + } else { + Err(Error::new( + eyre!("file {} missing from archive", path.as_ref().display()), + ErrorKind::ParseS9pk, + )) + } + } + pub fn check_stem( + &mut self, + path: impl AsRef, + mut valid_extension: impl FnMut(Option<&OsStr>) -> bool, + ) -> Result<(), Error> { + let (dir, stem) = + if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { + ( + self.dir + .get_path(parent) + .and_then(|e| e.as_directory()) + .ok_or_else(|| { + Error::new( + eyre!("directory {} missing from archive", parent.display()), + ErrorKind::ParseS9pk, + ) + })?, + path.as_ref().strip_prefix(parent).unwrap(), + ) + } else { + (self.dir, path.as_ref()) + }; + let name = dir + .with_stem(&stem.as_os_str().to_string_lossy()) + .filter(|(_, e)| e.as_file().is_some()) + .try_fold( + Err(Error::new( + eyre!( + "file {} with valid extension missing from archive", + path.as_ref().display() + ), + ErrorKind::ParseS9pk, + )), + |acc, (name, _)| + if valid_extension(Path::new(&*name).extension()) { + match acc { + Ok(_) => Err(Error::new( + eyre!( + "more than one file matching {} with valid extension in archive", + path.as_ref().display() + ), + ErrorKind::ParseS9pk, + )), + Err(_) => Ok(Ok(name)) + } + } else { + Ok(acc) + } + )??; + self.keep + .insert_path(path.as_ref().with_file_name(name), Entry::file(()))?; + Ok(()) + } + pub fn into_filter(self) -> Filter { + Filter(self.keep) + } +} + +pub struct Filter(DirectoryContents<()>); +impl Filter { + pub fn keep_checked( + &self, + dir: &mut DirectoryContents, + ) -> Result<(), Error> { + dir.filter(|path| self.0.get_path(path).is_some()) + } +} diff --git a/core/startos/src/s9pk/merkle_archive/file_contents.rs b/core/startos/src/s9pk/merkle_archive/file_contents.rs new file mode 100644 index 000000000..c34193e31 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/file_contents.rs @@ -0,0 +1,70 @@ +use blake3::Hash; +use tokio::io::AsyncRead; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::sink::Sink; +use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::CAP_10_MiB; + +#[derive(Debug, Clone)] +pub struct FileContents(S); +impl FileContents { + pub fn new(source: S) -> Self { + Self(source) + } + pub const fn header_size() -> u64 { + 8 // position: u64 BE + } +} +impl FileContents> { + #[instrument(skip_all)] + pub async fn deserialize( + source: S, + header: &mut (impl AsyncRead + Unpin + Send), + size: u64, + ) -> Result { + use tokio::io::AsyncReadExt; + + let mut position = [0u8; 8]; + header.read_exact(&mut position).await?; + let position = u64::from_be_bytes(position); + + Ok(Self(source.section(position, size))) + } +} +impl FileContents { + pub async fn hash(&self) -> Result<(Hash, u64), Error> { + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); + self.serialize_body(&mut hasher, None).await?; + let size = hasher.position(); + let hash = hasher.into_inner().finalize().await?; + Ok((hash, size)) + } + #[instrument(skip_all)] + pub async fn serialize_header(&self, position: u64, w: &mut W) -> Result { + use tokio::io::AsyncWriteExt; + + w.write_all(&position.to_be_bytes()).await?; + + Ok(position) + } + #[instrument(skip_all)] + pub async fn serialize_body( + &self, + w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.0.copy_verify(w, verify).await?; + Ok(()) + } + pub fn into_dyn(self) -> FileContents { + FileContents(DynFileSource::new(self.0)) + } +} +impl std::ops::Deref for FileContents { + type Target = S; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/core/startos/src/s9pk/merkle_archive/hash.rs b/core/startos/src/s9pk/merkle_archive/hash.rs new file mode 100644 index 000000000..c7ad470e4 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/hash.rs @@ -0,0 +1,92 @@ +use std::task::Poll; + +use blake3::Hash; +use tokio::io::AsyncWrite; +use tokio_util::either::Either; + +use crate::prelude::*; +use crate::util::io::{ParallelBlake3Writer, TeeWriter}; +use crate::CAP_10_MiB; + +#[pin_project::pin_project] +pub struct VerifyingWriter { + verify: Option<(Hash, u64)>, + #[pin] + writer: Either, W>, +} +impl VerifyingWriter { + pub fn new(w: W, verify: Option<(Hash, u64)>) -> Self { + Self { + writer: if verify.is_some() { + Either::Left(TeeWriter::new( + w, + ParallelBlake3Writer::new(CAP_10_MiB), + CAP_10_MiB, + )) + } else { + Either::Right(w) + }, + verify, + } + } +} +impl VerifyingWriter { + pub async fn verify(self) -> Result { + match self.writer { + Either::Left(writer) => { + let (writer, actual) = writer.into_inner().await?; + if let Some((expected, remaining)) = self.verify { + ensure_code!( + actual.finalize().await? == expected, + ErrorKind::InvalidSignature, + "hash sum mismatch" + ); + ensure_code!( + remaining == 0, + ErrorKind::InvalidSignature, + "file size mismatch" + ); + } + Ok(writer) + } + Either::Right(writer) => Ok(writer), + } + } +} +impl AsyncWrite for VerifyingWriter { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + if let Some((_, remaining)) = this.verify { + if *remaining < buf.len() as u64 { + return Poll::Ready(Err(std::io::Error::other(eyre!( + "attempted to write more bytes than signed" + )))); + } + } + match this.writer.poll_write(cx, buf)? { + Poll::Pending => Poll::Pending, + Poll::Ready(n) => { + if let Some((_, remaining)) = this.verify { + *remaining -= n as u64; + } + Poll::Ready(Ok(n)) + } + } + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().writer.poll_flush(cx) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().writer.poll_shutdown(cx) + } +} diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs new file mode 100644 index 000000000..3f30a4ce1 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -0,0 +1,449 @@ +use std::path::Path; + +use blake3::Hash; +use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; +use imbl_value::InternedString; +use sha2::{Digest, Sha512}; +use tokio::io::AsyncRead; + +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::SignatureScheme; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::file_contents::FileContents; +use crate::s9pk::merkle_archive::sink::Sink; +use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; +use crate::s9pk::merkle_archive::write_queue::WriteQueue; +use crate::util::serde::Base64; +use crate::CAP_1_MiB; + +pub mod directory_contents; +pub mod expected; +pub mod file_contents; +pub mod hash; +pub mod sink; +pub mod source; +#[cfg(test)] +mod test; +pub mod varint; +pub mod write_queue; + +#[derive(Debug, Clone)] +enum Signer { + Signed(VerifyingKey, Signature, u64, InternedString), + Signer(SigningKey, InternedString), +} + +#[derive(Debug, Clone)] +pub struct MerkleArchive { + signer: Signer, + contents: DirectoryContents, +} +impl MerkleArchive { + pub fn new(contents: DirectoryContents, signer: SigningKey, context: &str) -> Self { + Self { + signer: Signer::Signer(signer, context.into()), + contents, + } + } + pub fn signer(&self) -> VerifyingKey { + match &self.signer { + Signer::Signed(k, _, _, _) => *k, + Signer::Signer(k, _) => k.verifying_key(), + } + } + pub const fn header_size() -> u64 { + 32 // pubkey + + 64 // signature + + 32 // sighash + + 8 // size + + DirectoryContents::>::header_size() + } + pub fn contents(&self) -> &DirectoryContents { + &self.contents + } + pub fn contents_mut(&mut self) -> &mut DirectoryContents { + &mut self.contents + } + pub fn set_signer(&mut self, key: SigningKey, context: &str) { + self.signer = Signer::Signer(key, context.into()); + } + pub fn sort_by( + &mut self, + sort_by: impl Fn(&str, &str) -> std::cmp::Ordering + Send + Sync + 'static, + ) { + self.contents.sort_by(sort_by) + } +} +impl MerkleArchive> { + #[instrument(skip_all)] + pub async fn deserialize( + source: &S, + context: &str, + header: &mut (impl AsyncRead + Unpin + Send), + commitment: Option<&MerkleArchiveCommitment>, + ) -> Result { + use tokio::io::AsyncReadExt; + + let mut pubkey = [0u8; 32]; + header.read_exact(&mut pubkey).await?; + let pubkey = VerifyingKey::from_bytes(&pubkey)?; + + let mut signature = [0u8; 64]; + header.read_exact(&mut signature).await?; + let signature = Signature::from_bytes(&signature); + + let mut sighash = [0u8; 32]; + header.read_exact(&mut sighash).await?; + let sighash = Hash::from_bytes(sighash); + + let mut max_size = [0u8; 8]; + header.read_exact(&mut max_size).await?; + let max_size = u64::from_be_bytes(max_size); + + pubkey.verify_prehashed_strict( + Sha512::new_with_prefix(sighash.as_bytes()).chain_update(&u64::to_be_bytes(max_size)), + Some(context.as_bytes()), + &signature, + )?; + + if let Some(MerkleArchiveCommitment { + root_sighash, + root_maxsize, + }) = commitment + { + if sighash.as_bytes() != &**root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if max_size > *root_maxsize { + return Err(Error::new( + eyre!("root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + } else { + if max_size > CAP_1_MiB as u64 { + return Err(Error::new( + eyre!("root directory max size over 1MiB, cancelling download in case of DOS attack"), + ErrorKind::InvalidSignature, + )); + } + } + + let contents = DirectoryContents::deserialize(source, header, (sighash, max_size)).await?; + + Ok(Self { + signer: Signer::Signed(pubkey, signature, max_size, context.into()), + contents, + }) + } +} +impl MerkleArchive { + pub async fn update_hashes(&mut self, only_missing: bool) -> Result<(), Error> { + self.contents.update_hashes(only_missing).await + } + pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { + self.contents.filter(filter) + } + pub async fn commitment(&self) -> Result { + let root_maxsize = match self.signer { + Signer::Signed(_, _, s, _) => s, + _ => self.contents.toc_size(), + }; + let root_sighash = self.contents.sighash().await?; + Ok(MerkleArchiveCommitment { + root_sighash: Base64(*root_sighash.as_bytes()), + root_maxsize, + }) + } + pub async fn signature(&self) -> Result { + match &self.signer { + Signer::Signed(_, s, _, _) => Ok(*s), + Signer::Signer(k, context) => { + Ed25519.sign_commitment(k, &self.commitment().await?, context) + } + } + } + #[instrument(skip_all)] + pub async fn serialize(&self, w: &mut W, verify: bool) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + let commitment = self.commitment().await?; + + let (pubkey, signature) = match &self.signer { + Signer::Signed(pubkey, signature, _, _) => (*pubkey, *signature), + Signer::Signer(s, context) => { + (s.into(), Ed25519.sign_commitment(s, &commitment, context)?) + } + }; + + w.write_all(pubkey.as_bytes()).await?; + w.write_all(&signature.to_bytes()).await?; + w.write_all(&*commitment.root_sighash).await?; + w.write_all(&u64::to_be_bytes(commitment.root_maxsize)) + .await?; + let mut next_pos = w.current_position().await?; + next_pos += DirectoryContents::::header_size(); + self.contents.serialize_header(next_pos, w).await?; + next_pos += self.contents.toc_size(); + let mut queue = WriteQueue::new(next_pos); + self.contents.serialize_toc(&mut queue, w).await?; + queue.serialize(w, verify).await?; + Ok(()) + } + pub fn into_dyn(self) -> MerkleArchive { + MerkleArchive { + signer: self.signer, + contents: self.contents.into_dyn(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Entry { + hash: Option<(Hash, u64)>, + contents: EntryContents, +} +impl Entry { + pub fn new(contents: EntryContents) -> Self { + Self { + hash: None, + contents, + } + } + pub fn file(source: S) -> Self { + Self::new(EntryContents::File(FileContents::new(source))) + } + pub fn directory(directory: DirectoryContents) -> Self { + Self::new(EntryContents::Directory(directory)) + } + pub fn hash(&self) -> Option<(Hash, u64)> { + self.hash + } + pub fn as_contents(&self) -> &EntryContents { + &self.contents + } + pub fn as_file(&self) -> Option<&FileContents> { + match self.as_contents() { + EntryContents::File(f) => Some(f), + _ => None, + } + } + pub fn expect_file(&self) -> Result<&FileContents, Error> { + self.as_file() + .ok_or_else(|| Error::new(eyre!("not a file"), ErrorKind::ParseS9pk)) + } + pub fn as_directory(&self) -> Option<&DirectoryContents> { + match self.as_contents() { + EntryContents::Directory(d) => Some(d), + _ => None, + } + } + pub fn as_contents_mut(&mut self) -> &mut EntryContents { + self.hash = None; + &mut self.contents + } + pub fn into_contents(self) -> EntryContents { + self.contents + } + pub fn into_file(self) -> Option> { + match self.into_contents() { + EntryContents::File(f) => Some(f), + _ => None, + } + } + pub fn into_directory(self) -> Option> { + match self.into_contents() { + EntryContents::Directory(d) => Some(d), + _ => None, + } + } + pub fn header_size(&self) -> u64 { + 32 // hash + + 8 // size: u64 BE + + self.contents.header_size() + } +} +impl Entry> { + #[instrument(skip_all)] + pub async fn deserialize( + source: S, + header: &mut (impl AsyncRead + Unpin + Send), + ) -> Result { + use tokio::io::AsyncReadExt; + + let mut hash = [0u8; 32]; + header.read_exact(&mut hash).await?; + let hash = Hash::from_bytes(hash); + + let mut size = [0u8; 8]; + header.read_exact(&mut size).await?; + let size = u64::from_be_bytes(size); + + let contents = EntryContents::deserialize(source, header, (hash, size)).await?; + + Ok(Self { + hash: Some((hash, size)), + contents, + }) + } +} +impl Entry { + pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { + if let EntryContents::Directory(d) = &mut self.contents { + d.filter(filter)?; + } + Ok(()) + } + pub async fn update_hash(&mut self, only_missing: bool) -> Result<(), Error> { + if let EntryContents::Directory(d) = &mut self.contents { + d.update_hashes(only_missing).await?; + } + self.hash = Some(self.contents.hash().await?); + Ok(()) + } + pub async fn to_missing(&self) -> Result { + let hash = if let Some(hash) = self.hash { + hash + } else { + self.contents.hash().await? + }; + Ok(Self { + hash: Some(hash), + contents: EntryContents::Missing, + }) + } + #[instrument(skip_all)] + pub async fn serialize_header( + &self, + position: u64, + w: &mut W, + ) -> Result, Error> { + use tokio::io::AsyncWriteExt; + + let (hash, size) = if let Some(hash) = self.hash { + hash + } else { + self.contents.hash().await? + }; + w.write_all(hash.as_bytes()).await?; + w.write_all(&u64::to_be_bytes(size)).await?; + self.contents.serialize_header(position, w).await + } + pub fn into_dyn(self) -> Entry { + Entry { + hash: self.hash, + contents: self.contents.into_dyn(), + } + } +} +impl Entry { + pub async fn read_file_to_vec(&self) -> Result, Error> { + match self.as_contents() { + EntryContents::File(f) => Ok(f.to_vec(self.hash).await?), + EntryContents::Directory(_) => Err(Error::new( + eyre!("expected file, found directory"), + ErrorKind::ParseS9pk, + )), + EntryContents::Missing => { + Err(Error::new(eyre!("entry is missing"), ErrorKind::ParseS9pk)) + } + } + } +} + +#[derive(Debug, Clone)] +pub enum EntryContents { + Missing, + File(FileContents), + Directory(DirectoryContents), +} +impl EntryContents { + fn type_id(&self) -> u8 { + match self { + Self::Missing => 0, + Self::File(_) => 1, + Self::Directory(_) => 2, + } + } + pub fn header_size(&self) -> u64 { + 1 // type + + match self { + Self::Missing => 0, + Self::File(_) => FileContents::::header_size(), + Self::Directory(_) => DirectoryContents::::header_size(), + } + } + pub fn is_dir(&self) -> bool { + matches!(self, &EntryContents::Directory(_)) + } + pub fn is_missing(&self) -> bool { + matches!(self, &EntryContents::Missing) + } +} +impl EntryContents> { + #[instrument(skip_all)] + pub async fn deserialize( + source: S, + header: &mut (impl AsyncRead + Unpin + Send), + (hash, size): (Hash, u64), + ) -> Result { + use tokio::io::AsyncReadExt; + + let mut type_id = [0u8]; + header.read_exact(&mut type_id).await?; + match type_id[0] { + 0 => Ok(Self::Missing), + 1 => Ok(Self::File( + FileContents::deserialize(source, header, size).await?, + )), + 2 => Ok(Self::Directory( + DirectoryContents::deserialize(&source, header, (hash, size)).await?, + )), + id => Err(Error::new( + eyre!("Unknown type id {id} found in MerkleArchive"), + ErrorKind::ParseS9pk, + )), + } + } +} +impl EntryContents { + pub async fn hash(&self) -> Result<(Hash, u64), Error> { + match self { + Self::Missing => Err(Error::new( + eyre!("Cannot compute hash of missing file"), + ErrorKind::Pack, + )), + Self::File(f) => f.hash().await, + Self::Directory(d) => Ok((d.sighash().await?, d.toc_size())), + } + } + pub fn into_dyn(self) -> EntryContents { + match self { + Self::Missing => EntryContents::Missing, + Self::File(f) => EntryContents::File(f.into_dyn()), + Self::Directory(d) => EntryContents::Directory(d.into_dyn()), + } + } +} +impl EntryContents { + #[instrument(skip_all)] + pub async fn serialize_header( + &self, + position: u64, + w: &mut W, + ) -> Result, Error> { + use tokio::io::AsyncWriteExt; + + w.write_all(&[self.type_id()]).await?; + Ok(match self { + Self::Missing => None, + Self::File(f) => Some(f.serialize_header(position, w).await?), + Self::Directory(d) => Some(d.serialize_header(position, w).await?), + }) + } +} diff --git a/core/startos/src/s9pk/merkle_archive/sink.rs b/core/startos/src/s9pk/merkle_archive/sink.rs new file mode 100644 index 000000000..5357eb2d6 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/sink.rs @@ -0,0 +1,25 @@ +use tokio::io::{AsyncSeek, AsyncWrite}; + +use crate::prelude::*; +use crate::util::io::TrackingIO; + +#[async_trait::async_trait] +pub trait Sink: AsyncWrite + Unpin + Send { + async fn current_position(&mut self) -> Result; +} + +#[async_trait::async_trait] +impl Sink for S { + async fn current_position(&mut self) -> Result { + use tokio::io::AsyncSeekExt; + + Ok(self.stream_position().await?) + } +} + +#[async_trait::async_trait] +impl Sink for TrackingIO { + async fn current_position(&mut self) -> Result { + Ok(self.position()) + } +} diff --git a/core/startos/src/s9pk/merkle_archive/source/http.rs b/core/startos/src/s9pk/merkle_archive/source/http.rs new file mode 100644 index 000000000..e58208277 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/source/http.rs @@ -0,0 +1,191 @@ +use std::collections::BTreeSet; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use bytes::Bytes; +use futures::{Stream, TryStreamExt}; +use reqwest::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; +use reqwest::{Client, Url}; +use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf, Take}; +use tokio_util::io::StreamReader; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::TrackingIO; +use crate::util::Apply; + +pub struct HttpSource { + url: Url, + client: Client, + size: Option, + range_support: Result<(), Arc>>>>, +} +impl HttpSource { + pub async fn new(client: Client, url: Url) -> Result { + let head = client + .head(url.clone()) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)?; + let range_support = head + .headers() + .get(ACCEPT_RANGES) + .and_then(|s| s.to_str().ok()) + == Some("bytes") + && false; + let size = head + .headers() + .get(CONTENT_LENGTH) + .and_then(|s| s.to_str().ok()) + .and_then(|s| s.parse().ok()); + Ok(Self { + url, + client, + size, + range_support: if range_support { + Ok(()) + } else { + Err(Arc::new(Mutex::new(BTreeSet::new()))) + }, + }) + } +} +impl ArchiveSource for HttpSource { + type FetchReader = HttpReader; + type FetchAllReader = StreamReader>, Bytes>; + async fn size(&self) -> Option { + self.size + } + async fn fetch_all(&self) -> Result { + Ok(StreamReader::new( + self.client + .get(self.url.clone()) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed), + )) + } + async fn fetch(&self, position: u64, size: u64) -> Result { + match &self.range_support { + Ok(_) => Ok(HttpReader::Range( + StreamReader::new(if size > 0 { + self.client + .get(self.url.clone()) + .header(RANGE, format!("bytes={}-{}", position, position + size - 1)) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed) + } else { + futures::stream::empty().apply(boxed) + }) + .take(size), + )), + Err(pool) => { + fn get_reader_for( + pool: &Arc>>>, + position: u64, + ) -> Option> { + let mut lock = pool.lock().unwrap(); + let pos = lock.range(..position).last()?.position(); + lock.take(&pos) + } + let reader = get_reader_for(pool, position); + let mut reader = if let Some(reader) = reader { + reader + } else { + TrackingIO::new( + 0, + StreamReader::new( + self.client + .get(self.url.clone()) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed), + ), + ) + }; + if reader.position() < position { + let to_skip = position - reader.position(); + tokio::io::copy(&mut (&mut reader).take(to_skip), &mut tokio::io::sink()) + .await?; + } + Ok(HttpReader::Rangeless { + pool: pool.clone(), + reader: Some(reader.take(size)), + }) + } + } + } +} + +type BoxStream<'a, T> = Pin + Send + Sync + 'a>>; +fn boxed<'a, T>(stream: impl Stream + Send + Sync + 'a) -> BoxStream<'a, T> { + Box::pin(stream) +} +type HttpBodyReader = StreamReader>, Bytes>; + +#[pin_project::pin_project(project = HttpReaderProj, PinnedDrop)] +pub enum HttpReader { + Range(#[pin] Take), + Rangeless { + pool: Arc>>>, + #[pin] + reader: Option>>, + }, +} +impl AsyncRead for HttpReader { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.project() { + HttpReaderProj::Range(r) => r.poll_read(cx, buf), + HttpReaderProj::Rangeless { mut reader, .. } => { + let mut finished = false; + if let Some(reader) = reader.as_mut().as_pin_mut() { + let start = buf.filled().len(); + futures::ready!(reader.poll_read(cx, buf)?); + finished = start == buf.filled().len(); + } + if finished { + reader.take(); + } + Poll::Ready(Ok(())) + } + } + } +} +#[pin_project::pinned_drop] +impl PinnedDrop for HttpReader { + fn drop(self: Pin<&mut Self>) { + match self.project() { + HttpReaderProj::Range(_) => (), + HttpReaderProj::Rangeless { pool, mut reader } => { + if let Some(reader) = reader.take() { + pool.lock().unwrap().insert(reader.into_inner()); + } + } + } + } +} + +// type RangelessReader = StreamReader, Bytes>; diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs new file mode 100644 index 000000000..cc9623ab6 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -0,0 +1,417 @@ +use std::cmp::min; +use std::io::SeekFrom; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::Arc; + +use blake3::Hash; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use tokio::fs::File; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, Take}; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::util::io::{open_file, TmpDir}; + +pub mod http; +pub mod multi_cursor_file; + +pub trait FileSource: Send + Sync + Sized + 'static { + type Reader: AsyncRead + Unpin + Send; + type SliceReader: AsyncRead + Unpin + Send; + fn size(&self) -> impl Future> + Send; + fn reader(&self) -> impl Future> + Send; + fn slice( + &self, + position: u64, + size: u64, + ) -> impl Future> + Send; + fn copy( + &self, + w: &mut W, + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.reader().await?, w).await?; + Ok(()) + } + } + fn copy_verify( + &self, + w: &mut W, + verify: Option<(Hash, u64)>, + ) -> impl Future> + Send { + async move { + let mut w = VerifyingWriter::new(w, verify); + tokio::io::copy(&mut self.reader().await?, &mut w).await?; + w.verify().await?; + Ok(()) + } + } + fn to_vec( + &self, + verify: Option<(Hash, u64)>, + ) -> impl Future, Error>> + Send { + fn to_vec( + src: &impl FileSource, + verify: Option<(Hash, u64)>, + ) -> BoxFuture, Error>> { + async move { + let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify { + *size + } else { + src.size().await? + } as usize); + src.copy_verify(&mut vec, verify).await?; + Ok(vec) + } + .boxed() + } + to_vec(self, verify) + } +} + +impl FileSource for Arc { + type Reader = T::Reader; + type SliceReader = T::SliceReader; + async fn size(&self) -> Result { + self.deref().size().await + } + async fn reader(&self) -> Result { + self.deref().reader().await + } + async fn slice(&self, position: u64, size: u64) -> Result { + self.deref().slice(position, size).await + } + async fn copy(&self, w: &mut W) -> Result<(), Error> { + self.deref().copy(w).await + } + async fn copy_verify( + &self, + w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.deref().copy_verify(w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + self.deref().to_vec(verify).await + } +} + +#[derive(Clone)] +pub struct DynFileSource(Arc); +impl DynFileSource { + pub fn new(source: T) -> Self { + Self(Arc::new(source)) + } +} +impl FileSource for DynFileSource { + type Reader = Box; + type SliceReader = Box; + async fn size(&self) -> Result { + self.0.size().await + } + async fn reader(&self) -> Result { + self.0.reader().await + } + async fn slice(&self, position: u64, size: u64) -> Result { + self.0.slice(position, size).await + } + async fn copy( + &self, + mut w: &mut W, + ) -> Result<(), Error> { + self.0.copy(&mut w).await + } + async fn copy_verify( + &self, + mut w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.0.copy_verify(&mut w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + self.0.to_vec(verify).await + } +} + +#[async_trait::async_trait] +trait DynableFileSource: Send + Sync + 'static { + async fn size(&self) -> Result; + async fn reader(&self) -> Result, Error>; + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error>; + async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error>; + async fn copy_verify( + &self, + w: &mut (dyn AsyncWrite + Unpin + Send), + verify: Option<(Hash, u64)>, + ) -> Result<(), Error>; + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error>; +} +#[async_trait::async_trait] +impl DynableFileSource for T { + async fn size(&self) -> Result { + FileSource::size(self).await + } + async fn reader(&self) -> Result, Error> { + Ok(Box::new(FileSource::reader(self).await?)) + } + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error> { + Ok(Box::new(FileSource::slice(self, position, size).await?)) + } + async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error> { + FileSource::copy(self, w).await + } + async fn copy_verify( + &self, + w: &mut (dyn AsyncWrite + Unpin + Send), + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + FileSource::copy_verify(self, w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + FileSource::to_vec(self, verify).await + } +} + +impl FileSource for PathBuf { + type Reader = File; + type SliceReader = Take; + async fn size(&self) -> Result { + Ok(tokio::fs::metadata(self).await?.len()) + } + async fn reader(&self) -> Result { + Ok(open_file(self).await?) + } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } +} + +impl FileSource for Arc<[u8]> { + type Reader = std::io::Cursor; + type SliceReader = Take; + async fn size(&self) -> Result { + Ok(self.len() as u64) + } + async fn reader(&self) -> Result { + Ok(std::io::Cursor::new(self.clone())) + } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } + async fn copy(&self, w: &mut W) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + w.write_all(&*self).await?; + Ok(()) + } +} + +pub trait ArchiveSource: Send + Sync + Sized + 'static { + type FetchReader: AsyncRead + Unpin + Send; + type FetchAllReader: AsyncRead + Unpin + Send; + fn size(&self) -> impl Future> + Send { + async { None } + } + fn fetch_all(&self) -> impl Future> + Send; + fn fetch( + &self, + position: u64, + size: u64, + ) -> impl Future> + Send; + fn copy_all_to( + &self, + w: &mut W, + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.fetch_all().await?, w).await?; + Ok(()) + } + } + fn copy_to( + &self, + position: u64, + size: u64, + w: &mut W, + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.fetch(position, size).await?, w).await?; + Ok(()) + } + } + fn section(self, position: u64, size: u64) -> Section { + Section { + source: self, + position, + size, + } + } +} + +impl ArchiveSource for Arc { + type FetchReader = T::FetchReader; + type FetchAllReader = T::FetchAllReader; + async fn size(&self) -> Option { + self.deref().size().await + } + async fn fetch_all(&self) -> Result { + self.deref().fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.deref().fetch(position, size).await + } + async fn copy_all_to( + &self, + w: &mut W, + ) -> Result<(), Error> { + self.deref().copy_all_to(w).await + } + async fn copy_to( + &self, + position: u64, + size: u64, + w: &mut W, + ) -> Result<(), Error> { + self.deref().copy_to(position, size, w).await + } +} + +impl ArchiveSource for Arc<[u8]> { + type FetchReader = tokio::io::Take>; + type FetchAllReader = std::io::Cursor; + async fn fetch_all(&self) -> Result { + Ok(std::io::Cursor::new(self.clone())) + } + async fn fetch(&self, position: u64, size: u64) -> Result { + use tokio::io::AsyncReadExt; + + let mut cur = std::io::Cursor::new(self.clone()); + cur.set_position(position); + Ok(cur.take(size)) + } +} + +#[derive(Debug, Clone)] +pub struct Section { + source: S, + position: u64, + size: u64, +} +impl FileSource for Section { + type Reader = S::FetchReader; + type SliceReader = S::FetchReader; + async fn size(&self) -> Result { + Ok(self.size) + } + async fn reader(&self) -> Result { + self.source.fetch(self.position, self.size).await + } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source + .fetch(self.position + position, min(size, self.size)) + .await + } + async fn copy(&self, w: &mut W) -> Result<(), Error> { + self.source.copy_to(self.position, self.size, w).await + } +} + +pub type DynRead = Box; +pub fn into_dyn_read(r: R) -> DynRead { + Box::new(r) +} + +#[derive(Clone)] +pub struct TmpSource { + tmp_dir: Arc, + source: S, +} +impl TmpSource { + pub fn new(tmp_dir: Arc, source: S) -> Self { + Self { tmp_dir, source } + } + pub async fn gc(self) -> Result<(), Error> { + self.tmp_dir.gc().await + } +} +impl std::ops::Deref for TmpSource { + type Target = S; + fn deref(&self) -> &Self::Target { + &self.source + } +} +impl ArchiveSource for TmpSource { + type FetchReader = ::FetchReader; + type FetchAllReader = ::FetchAllReader; + async fn size(&self) -> Option { + self.source.size().await + } + async fn fetch_all(&self) -> Result { + self.source.fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.source.fetch(position, size).await + } + async fn copy_all_to( + &self, + w: &mut W, + ) -> Result<(), Error> { + self.source.copy_all_to(w).await + } + async fn copy_to( + &self, + position: u64, + size: u64, + w: &mut W, + ) -> Result<(), Error> { + self.source.copy_to(position, size, w).await + } +} +impl From> for DynFileSource { + fn from(value: TmpSource) -> Self { + DynFileSource::new(value) + } +} + +impl FileSource for TmpSource { + type Reader = ::Reader; + type SliceReader = ::SliceReader; + async fn size(&self) -> Result { + self.source.size().await + } + async fn reader(&self) -> Result { + self.source.reader().await + } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source.slice(position, size).await + } + async fn copy( + &self, + mut w: &mut W, + ) -> Result<(), Error> { + self.source.copy(&mut w).await + } + async fn copy_verify( + &self, + mut w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.source.copy_verify(&mut w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + self.source.to_vec(verify).await + } +} diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs new file mode 100644 index 000000000..658f3f923 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -0,0 +1,146 @@ +use std::io::SeekFrom; +use std::os::fd::{AsRawFd, RawFd}; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Poll; + +use tokio::fs::File; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, ReadBuf, Take}; +use tokio::sync::{Mutex, OwnedMutexGuard}; + +use crate::disk::mount::filesystem::loop_dev::LoopDev; +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; +use crate::util::io::open_file; + +fn path_from_fd(fd: RawFd) -> Result { + #[cfg(target_os = "linux")] + let path = Path::new("/proc/self/fd").join(fd.to_string()); + #[cfg(target_os = "macos")] // here be dragons + let path = unsafe { + let mut buf = [0u8; libc::PATH_MAX as usize]; + if libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr().cast::()) == -1 { + return Err(std::io::Error::last_os_error().into()); + } + Path::new( + &*std::ffi::CStr::from_bytes_until_nul(&buf) + .with_kind(ErrorKind::Utf8)? + .to_string_lossy(), + ) + .to_owned() + }; + Ok(path) +} + +#[derive(Clone)] +pub struct MultiCursorFile { + fd: RawFd, + file: Arc>, +} +impl MultiCursorFile { + fn path(&self) -> Result { + path_from_fd(self.fd) + } + pub async fn open(fd: &impl AsRawFd) -> Result { + let f = open_file(path_from_fd(fd.as_raw_fd())?).await?; + Ok(Self::from(f)) + } + pub async fn cursor(&self) -> Result { + Ok(FileCursor( + if let Ok(file) = self.file.clone().try_lock_owned() { + file + } else { + Arc::new(Mutex::new(open_file(self.path()?).await?)) + .try_lock_owned() + .expect("freshly created") + }, + )) + } + pub async fn blake3_mmap(&self) -> Result { + let path = self.path()?; + tokio::task::spawn_blocking(move || { + let mut hasher = blake3::Hasher::new(); + hasher.update_mmap_rayon(path)?; + Ok(hasher.finalize()) + }) + .await + .with_kind(ErrorKind::Unknown)? + } +} +impl From for MultiCursorFile { + fn from(value: File) -> Self { + Self { + fd: value.as_raw_fd(), + file: Arc::new(Mutex::new(value)), + } + } +} + +#[pin_project::pin_project] +pub struct FileCursor(#[pin] OwnedMutexGuard); +impl AsyncRead for FileCursor { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + Pin::new(&mut (&mut **this.0.get_mut())).poll_read(cx, buf) + } +} +impl AsyncSeek for FileCursor { + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { + let this = self.project(); + Pin::new(&mut (&mut **this.0.get_mut())).start_seek(position) + } + fn poll_complete( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + Pin::new(&mut (&mut **this.0.get_mut())).poll_complete(cx) + } +} +impl std::ops::Deref for FileCursor { + type Target = File; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} +impl std::ops::DerefMut for FileCursor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.0 + } +} + +impl ArchiveSource for MultiCursorFile { + type FetchReader = Take; + type FetchAllReader = FileCursor; + async fn size(&self) -> Option { + tokio::fs::metadata(self.path().ok()?) + .await + .ok() + .map(|m| m.len()) + } + async fn fetch_all(&self) -> Result { + use tokio::io::AsyncSeekExt; + + let mut file = self.cursor().await?; + file.0.seek(SeekFrom::Start(0)).await?; + Ok(file) + } + async fn fetch(&self, position: u64, size: u64) -> Result { + use tokio::io::AsyncSeekExt; + + let mut file = self.cursor().await?; + file.0.seek(SeekFrom::Start(position)).await?; + Ok(file.take(size)) + } +} + +impl From<&Section> for LoopDev { + fn from(value: &Section) -> Self { + LoopDev::new(value.source.path().unwrap(), value.position, value.size) + } +} diff --git a/core/startos/src/s9pk/merkle_archive/test.rs b/core/startos/src/s9pk/merkle_archive/test.rs new file mode 100644 index 000000000..861f3b04c --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/test.rs @@ -0,0 +1,142 @@ +use std::collections::BTreeMap; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use ed25519_dalek::SigningKey; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::file_contents::FileContents; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::{Entry, EntryContents, MerkleArchive}; +use crate::util::io::TrackingIO; + +/// Creates a MerkleArchive (a1) with the provided files at the provided paths. NOTE: later files can overwrite previous files/directories at the same path +/// Tests: +/// - a1.update_hashes(): returns Ok(_) +/// - a1.serialize(verify: true): returns Ok(s1) +/// - MerkleArchive::deserialize(s1): returns Ok(a2) +/// - a2: contains all expected files with expected content +/// - a2.serialize(verify: true): returns Ok(s2) +/// - s1 == s2 +#[instrument] +fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> { + let mut root = DirectoryContents::>::new(); + let mut check_set = BTreeMap::::new(); + for (path, content) in files { + if let Err(e) = root.insert_path( + &path, + Entry::new(EntryContents::File(FileContents::new( + content.clone().into_bytes().into(), + ))), + ) { + eprintln!("failed to insert file at {path:?}: {e}"); + } else { + let path = path.strip_prefix("/").unwrap_or(&path); + let mut remaining = check_set.split_off(path); + while { + if let Some((p, s)) = remaining.pop_first() { + if !p.starts_with(path) { + remaining.insert(p, s); + false + } else { + true + } + } else { + false + } + } {} + check_set.append(&mut remaining); + check_set.insert(path.to_owned(), content); + } + } + let key = SigningKey::generate(&mut rand::thread_rng()); + let mut a1 = MerkleArchive::new(root, key, "test"); + tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap() + .block_on(async move { + a1.update_hashes(true).await?; + let mut s1 = Vec::new(); + a1.serialize(&mut TrackingIO::new(0, &mut s1), true).await?; + let s1: Arc<[u8]> = s1.into(); + let a2 = MerkleArchive::deserialize( + &s1, + "test", + &mut Cursor::new(s1.clone()), + Some(&a1.commitment().await?), + ) + .await?; + + for (path, content) in check_set { + match a2 + .contents + .get_path(&path) + .map(|e| (e.as_contents(), e.hash())) + { + Some((EntryContents::File(f), hash)) => { + ensure_code!( + &f.to_vec(hash).await? == content.as_bytes(), + ErrorKind::ParseS9pk, + "File at {path:?} does not match input" + ) + } + _ => { + return Err(Error::new( + eyre!("expected file at {path:?}"), + ErrorKind::ParseS9pk, + )) + } + } + } + + let mut s2 = Vec::new(); + a2.serialize(&mut TrackingIO::new(0, &mut s2), true).await?; + let s2: Arc<[u8]> = s2.into(); + ensure_code!(s1 == s2, ErrorKind::Pack, "s1 does not match s2"); + + Ok(()) + }) +} + +proptest::proptest! { + #[test] + fn property_test(files: Vec<(PathBuf, String)>) { + let files: Vec<(PathBuf, String)> = files.into_iter().filter(|(p, _)| p.file_name().is_some() && p.iter().all(|s| s.to_str().is_some())).collect(); + if let Err(e) = test(files.clone()) { + panic!("{e}\nInput: {files:#?}\n{e:?}"); + } + } +} + +#[test] +fn test_example_1() { + if let Err(e) = test(vec![(Path::new("foo").into(), "bar".into())]) { + panic!("{e}\n{e:?}"); + } +} + +#[test] +fn test_example_2() { + if let Err(e) = test(vec![ + (Path::new("a/a.txt").into(), "a.txt".into()), + (Path::new("a/b/a.txt").into(), "a.txt".into()), + (Path::new("a/b/b/a.txt").into(), "a.txt".into()), + (Path::new("a/b/c.txt").into(), "c.txt".into()), + (Path::new("a/c.txt").into(), "c.txt".into()), + ]) { + panic!("{e}\n{e:?}"); + } +} + +#[test] +fn test_example_3() { + if let Err(e) = test(vec![ + (Path::new("b/a").into(), "𑦪".into()), + (Path::new("a/c/a").into(), "·".into()), + ]) { + panic!("{e}\n{e:?}"); + } +} diff --git a/core/startos/src/s9pk/merkle_archive/varint.rs b/core/startos/src/s9pk/merkle_archive/varint.rs new file mode 100644 index 000000000..f4f18d140 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/varint.rs @@ -0,0 +1,157 @@ +use integer_encoding::VarInt; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::prelude::*; + +/// Most-significant bit, == 0x80 +pub const MSB: u8 = 0b1000_0000; + +const MAX_STR_LEN: u64 = 1024 * 1024; // 1 MiB + +pub fn serialized_varint_size(n: u64) -> u64 { + VarInt::required_space(n) as u64 +} + +pub async fn serialize_varint( + n: u64, + w: &mut W, +) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + let mut buf = [0 as u8; 10]; + let b = n.encode_var(&mut buf); + w.write_all(&buf[0..b]).await?; + + Ok(()) +} + +pub fn serialized_varstring_size(s: &str) -> u64 { + serialized_varint_size(s.len() as u64) + s.len() as u64 +} + +pub async fn serialize_varstring( + s: &str, + w: &mut W, +) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + serialize_varint(s.len() as u64, w).await?; + w.write_all(s.as_bytes()).await?; + Ok(()) +} + +const MAX_SIZE: usize = (std::mem::size_of::() * 8 + 7) / 7; + +#[derive(Default)] +struct VarIntProcessor { + buf: [u8; MAX_SIZE], + i: usize, +} + +impl VarIntProcessor { + fn new() -> VarIntProcessor { + Self::default() + } + fn push(&mut self, b: u8) -> Result<(), Error> { + if self.i >= MAX_SIZE { + return Err(Error::new( + eyre!("Unterminated varint"), + ErrorKind::ParseS9pk, + )); + } + self.buf[self.i] = b; + self.i += 1; + Ok(()) + } + fn finished(&self) -> bool { + self.i > 0 && (self.buf[self.i - 1] & MSB == 0) + } + fn decode(&self) -> Option { + Some(u64::decode_var(&self.buf[0..self.i])?.0) + } +} + +pub async fn deserialize_varint(r: &mut R) -> Result { + use tokio::io::AsyncReadExt; + + let mut buf = [0 as u8; 1]; + let mut p = VarIntProcessor::new(); + + while !p.finished() { + r.read_exact(&mut buf).await?; + + p.push(buf[0])?; + } + + p.decode() + .ok_or_else(|| Error::new(eyre!("Reached EOF"), ErrorKind::ParseS9pk)) +} + +pub async fn deserialize_varstring(r: &mut R) -> Result { + use tokio::io::AsyncReadExt; + + let len = std::cmp::min(deserialize_varint(r).await?, MAX_STR_LEN); + let mut res = String::with_capacity(len as usize); + r.take(len).read_to_string(&mut res).await?; + Ok(res) +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::prelude::*; + + fn test_int(n: u64) -> Result<(), Error> { + let n1 = n; + tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap() + .block_on(async move { + let mut v = Vec::new(); + super::serialize_varint(n1, &mut v).await?; + let n2 = super::deserialize_varint(&mut Cursor::new(v)).await?; + + ensure_code!(n1 == n2, ErrorKind::Deserialization, "n1 does not match n2"); + + Ok(()) + }) + } + + fn test_string(s: &str) -> Result<(), Error> { + let s1 = s; + tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap() + .block_on(async move { + let mut v: Vec = Vec::new(); + super::serialize_varstring(&s1, &mut v).await?; + let s2 = super::deserialize_varstring(&mut Cursor::new(v)).await?; + + ensure_code!( + s1 == &s2, + ErrorKind::Deserialization, + "s1 does not match s2" + ); + + Ok(()) + }) + } + + proptest::proptest! { + #[test] + fn proptest_int(n: u64) { + if let Err(e) = test_int(n) { + panic!("{e}\nInput: {n}\n{e:?}"); + } + } + + #[test] + fn proptest_string(s: String) { + if let Err(e) = test_string(&s) { + panic!("{e}\nInput: {s:?}\n{e:?}"); + } + } + } +} diff --git a/core/startos/src/s9pk/merkle_archive/write_queue.rs b/core/startos/src/s9pk/merkle_archive/write_queue.rs new file mode 100644 index 000000000..4e1bb3a73 --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/write_queue.rs @@ -0,0 +1,48 @@ +use std::collections::VecDeque; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::sink::Sink; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::{Entry, EntryContents}; + +pub struct WriteQueue<'a, S> { + next_available_position: u64, + queue: VecDeque<&'a Entry>, +} + +impl<'a, S> WriteQueue<'a, S> { + pub fn new(next_available_position: u64) -> Self { + Self { + next_available_position, + queue: VecDeque::new(), + } + } +} +impl<'a, S: FileSource> WriteQueue<'a, S> { + pub async fn add(&mut self, entry: &'a Entry) -> Result { + let res = self.next_available_position; + let size = match entry.as_contents() { + EntryContents::Missing => return Ok(0), + EntryContents::File(f) => f.size().await?, + EntryContents::Directory(d) => d.toc_size(), + }; + self.next_available_position += size; + self.queue.push_back(entry); + Ok(res) + } +} +impl<'a, S: FileSource + Clone> WriteQueue<'a, S> { + pub async fn serialize(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { + loop { + let Some(next) = self.queue.pop_front() else { + break; + }; + match next.as_contents() { + EntryContents::Missing => (), + EntryContents::File(f) => f.serialize_body(w, next.hash.filter(|_| verify)).await?, + EntryContents::Directory(d) => d.serialize_toc(self, w).await?, + } + } + Ok(()) + } +} diff --git a/core/startos/src/s9pk/mod.rs b/core/startos/src/s9pk/mod.rs index e1bf4caba..a06218d40 100644 --- a/core/startos/src/s9pk/mod.rs +++ b/core/startos/src/s9pk/mod.rs @@ -1,246 +1,60 @@ -use std::ffi::OsStr; -use std::path::PathBuf; - -use color_eyre::eyre::eyre; -use futures::TryStreamExt; -use imbl::OrdMap; -use rpc_toolkit::command; -use serde_json::Value; -use tokio::io::AsyncRead; -use tracing::instrument; - -use crate::context::SdkContext; -use crate::s9pk::builder::S9pkPacker; -use crate::s9pk::docker::DockerMultiArch; -use crate::s9pk::git_hash::GitHash; -use crate::s9pk::manifest::Manifest; -use crate::s9pk::reader::S9pkReader; -use crate::util::display_none; -use crate::util::io::BufferedWriteReader; -use crate::util::serde::IoFormat; -use crate::volume::Volume; -use crate::{Error, ErrorKind, ResultExt}; - -pub mod builder; -pub mod docker; pub mod git_hash; -pub mod header; -pub mod manifest; -pub mod reader; - -pub const SIG_CONTEXT: &[u8] = b"s9pk"; - -#[command(cli_only, display(display_none))] -#[instrument(skip_all)] -pub async fn pack(#[context] ctx: SdkContext, #[arg] path: Option) -> Result<(), Error> { - use tokio::fs::File; - - let path = if let Some(path) = path { - path - } else { - std::env::current_dir()? - }; - let manifest_value: Value = if path.join("manifest.toml").exists() { - IoFormat::Toml - .from_async_reader(File::open(path.join("manifest.toml")).await?) - .await? - } else if path.join("manifest.yaml").exists() { - IoFormat::Yaml - .from_async_reader(File::open(path.join("manifest.yaml")).await?) - .await? - } else if path.join("manifest.json").exists() { - IoFormat::Json - .from_async_reader(File::open(path.join("manifest.json")).await?) - .await? - } else { - return Err(Error::new( - eyre!("manifest not found"), - crate::ErrorKind::Pack, - )); - }; - - let manifest: Manifest = serde_json::from_value::(manifest_value.clone()) - .with_kind(crate::ErrorKind::Deserialization)? - .with_git_hash(GitHash::from_path(&path).await?); - let extra_keys = - enumerate_extra_keys(&serde_json::to_value(&manifest).unwrap(), &manifest_value); - for k in extra_keys { - tracing::warn!("Unrecognized Manifest Key: {}", k); - } - - let outfile_path = path.join(format!("{}.s9pk", manifest.id)); - let mut outfile = File::create(outfile_path).await?; - S9pkPacker::builder() - .manifest(&manifest) - .writer(&mut outfile) - .license( - File::open(path.join(manifest.assets.license_path())) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - manifest.assets.license_path().display().to_string(), - ) - })?, - ) - .icon( - File::open(path.join(manifest.assets.icon_path())) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - manifest.assets.icon_path().display().to_string(), - ) - })?, +pub mod merkle_archive; +pub mod rpc; +pub mod v1; +pub mod v2; + +use std::sync::Arc; + +use tokio::io::{AsyncReadExt, AsyncSeek}; +pub use v2::{manifest, S9pk}; + +use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource}; +use crate::s9pk::v1::reader::S9pkReader; +use crate::s9pk::v2::compat::MAGIC_AND_VERSION; +use crate::util::io::TmpDir; + +pub async fn load( + source: S, + key: K, + progress: Option<&FullProgressTracker>, +) -> Result, Error> +where + S: ArchiveSource, + S::FetchAllReader: AsyncSeek + Sync, + K: FnOnce() -> Result, +{ + // TODO: return s9pk + const MAGIC_LEN: usize = MAGIC_AND_VERSION.len(); + let mut magic = [0_u8; MAGIC_LEN]; + source.fetch(0, 3).await?.read_exact(&mut magic).await?; + if magic == v2::compat::MAGIC_AND_VERSION { + let phase = if let Some(progress) = progress { + let mut phase = progress.add_phase( + "Converting Package to V2".into(), + Some(source.size().await.unwrap_or(60)), + ); + phase.start(); + Some(phase) + } else { + None + }; + tracing::info!("Converting package to v2 s9pk"); + let tmp_dir = TmpDir::new().await?; + let s9pk = S9pk::from_v1( + S9pkReader::from_reader(source.fetch_all().await?, true).await?, + Arc::new(tmp_dir), + key()?, ) - .instructions( - File::open(path.join(manifest.assets.instructions_path())) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - manifest.assets.instructions_path().display().to_string(), - ) - })?, - ) - .docker_images({ - let docker_images_path = path.join(manifest.assets.docker_images_path()); - let res: Box = if tokio::fs::metadata(&docker_images_path).await?.is_dir() { - let tars: Vec<_> = tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(&docker_images_path).await?).try_collect().await?; - let mut arch_info = DockerMultiArch::default(); - for tar in &tars { - if tar.path().extension() == Some(OsStr::new("tar")) { - arch_info.available.insert(tar.path().file_stem().unwrap_or_default().to_str().unwrap_or_default().to_owned()); - } - } - if arch_info.available.contains("aarch64") { - arch_info.default = "aarch64".to_owned(); - } else { - arch_info.default = arch_info.available.iter().next().cloned().unwrap_or_default(); - } - let arch_info_cbor = IoFormat::Cbor.to_vec(&arch_info)?; - Box::new(BufferedWriteReader::new(|w| async move { - let mut docker_images = tokio_tar::Builder::new(w); - let mut multiarch_header = tokio_tar::Header::new_gnu(); - multiarch_header.set_path("multiarch.cbor")?; - multiarch_header.set_size(arch_info_cbor.len() as u64); - multiarch_header.set_cksum(); - docker_images.append(&multiarch_header, std::io::Cursor::new(arch_info_cbor)).await?; - for tar in tars - { - docker_images - .append_path_with_name( - tar.path(), - tar.file_name(), - ) - .await?; - } - Ok::<_, std::io::Error>(()) - }, 1024 * 1024)) - } else { - Box::new(File::open(docker_images_path) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - manifest.assets.docker_images_path().display().to_string(), - ) - })?) - }; - res - }) - .assets({ - let asset_volumes = manifest - .volumes - .iter() - .filter(|(_, v)| matches!(v, &&Volume::Assets {})).map(|(id, _)| id.clone()).collect::>(); - let assets_path = manifest.assets.assets_path().to_owned(); - let path = path.clone(); - - BufferedWriteReader::new(|w| async move { - let mut assets = tokio_tar::Builder::new(w); - for asset_volume in asset_volumes - { - assets - .append_dir_all( - &asset_volume, - path.join(&assets_path).join(&asset_volume), - ) - .await?; - } - Ok::<_, std::io::Error>(()) - }, 1024 * 1024) - }) - .scripts({ - let script_path = path.join(manifest.assets.scripts_path()).join("embassy.js"); - let needs_script = manifest.package_procedures().any(|a| a.is_script()); - let has_script = script_path.exists(); - match (needs_script, has_script) { - (true, true) => Some(File::open(script_path).await?), - (true, false) => { - return Err(Error::new(eyre!("Script is declared in manifest, but no such script exists at ./scripts/embassy.js"), ErrorKind::Pack).into()) - } - (false, true) => { - tracing::warn!("Manifest does not declare any actions that use scripts, but a script exists at ./scripts/embassy.js"); - None - } - (false, false) => None - } - }) - .build() - .pack(&ctx.developer_key()?) .await?; - outfile.sync_all().await?; - - Ok(()) -} - -#[command(rename = "s9pk", cli_only, display(display_none))] -pub async fn verify(#[arg] path: PathBuf) -> Result<(), Error> { - let mut s9pk = S9pkReader::open(path, true).await?; - s9pk.validate().await?; - - Ok(()) -} - -fn enumerate_extra_keys(reference: &Value, candidate: &Value) -> Vec { - match (reference, candidate) { - (Value::Object(m_r), Value::Object(m_c)) => { - let om_r: OrdMap = m_r.clone().into_iter().collect(); - let om_c: OrdMap = m_c.clone().into_iter().collect(); - let common = om_r.clone().intersection(om_c.clone()); - let top_extra = common.clone().symmetric_difference(om_c.clone()); - let mut all_extra = top_extra - .keys() - .map(|s| format!(".{}", s)) - .collect::>(); - for (k, v) in common { - all_extra.extend( - enumerate_extra_keys(&v, om_c.get(&k).unwrap()) - .into_iter() - .map(|s| format!(".{}{}", k, s)), - ) - } - all_extra + tracing::info!("Converted s9pk successfully"); + if let Some(mut phase) = phase { + phase.complete(); } - (_, Value::Object(m1)) => m1.clone().keys().map(|s| format!(".{}", s)).collect(), - _ => Vec::new(), + Ok(s9pk.into_dyn()) + } else { + Ok(S9pk::deserialize(&Arc::new(source), None).await?.into_dyn()) } } - -#[test] -fn test_enumerate_extra_keys() { - use serde_json::json; - let extras = enumerate_extra_keys( - &json!({ - "test": 1, - "test2": null, - }), - &json!({ - "test": 1, - "test2": { "test3": null }, - "test4": null - }), - ); - println!("{:?}", extras) -} diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs new file mode 100644 index 000000000..98b46ac77 --- /dev/null +++ b/core/startos/src/s9pk/rpc.rs @@ -0,0 +1,254 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use clap::Parser; +use models::ImageId; +use rpc_toolkit::{from_fn_async, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::s9pk::manifest::Manifest; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::v2::pack::ImageConfig; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::util::io::{create_file, open_file, TmpDir}; +use crate::util::serde::{apply_expr, HandlerExtSerde}; +use crate::util::Apply; + +pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; + +pub fn s9pk() -> ParentHandler { + ParentHandler::new() + .subcommand( + "pack", + from_fn_async(super::v2::pack::pack) + .no_display() + .with_about("Package s9pk input files into valid s9pk"), + ) + .subcommand( + "list-ingredients", + from_fn_async(super::v2::pack::list_ingredients) + .with_custom_display_fn(|_, ingredients| { + ingredients + .into_iter() + .map(Some) + .apply(|i| itertools::intersperse(i, None)) + .for_each(|i| { + if let Some(p) = i { + print!("{}", p.display()) + } else { + print!(" ") + } + }); + println!(); + Ok(()) + }) + .with_about("List paths of package ingredients"), + ) + .subcommand( + "edit", + edit().with_about("Commands to add an image to an s9pk or edit the manifest"), + ) + .subcommand( + "inspect", + inspect().with_about("Commands to display file paths, file contents, or manifest"), + ) + .subcommand( + "convert", + from_fn_async(convert) + .no_display() + .with_about("Convert s9pk from v1 to v2"), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +struct S9pkPath { + s9pk: PathBuf, +} + +fn edit() -> ParentHandler { + let only_parent = |a, _| a; + ParentHandler::new() + .subcommand( + "add-image", + from_fn_async(add_image) + .with_inherited(only_parent) + .no_display() + .with_about("Add image to s9pk"), + ) + .subcommand( + "manifest", + from_fn_async(edit_manifest) + .with_inherited(only_parent) + .with_display_serializable() + .with_about("Edit s9pk manifest"), + ) +} + +fn inspect() -> ParentHandler { + let only_parent = |a, _| a; + ParentHandler::new() + .subcommand( + "file-tree", + from_fn_async(file_tree) + .with_inherited(only_parent) + .with_display_serializable() + .with_about("Display list of paths"), + ) + .subcommand( + "cat", + from_fn_async(cat) + .with_inherited(only_parent) + .no_display() + .with_about("Display file contents"), + ) + .subcommand( + "manifest", + from_fn_async(inspect_manifest) + .with_inherited(only_parent) + .with_display_serializable() + .with_about("Display s9pk manifest"), + ) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +struct AddImageParams { + id: ImageId, + #[command(flatten)] + config: ImageConfig, +} +async fn add_image( + ctx: CliContext, + AddImageParams { id, config }: AddImageParams, + S9pkPath { s9pk: s9pk_path }: S9pkPath, +) -> Result<(), Error> { + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + s9pk.as_manifest_mut().images.insert(id, config); + let tmp_dir = Arc::new(TmpDir::new().await?); + s9pk.load_images(tmp_dir.clone()).await?; + s9pk.validate_and_filter(None)?; + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + let mut tmp_file = create_file(&tmp_path).await?; + s9pk.serialize(&mut tmp_file, true).await?; + drop(s9pk); + tmp_file.sync_all().await?; + tokio::fs::rename(&tmp_path, &s9pk_path).await?; + + tmp_dir.gc().await?; + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +struct EditManifestParams { + expression: String, +} +async fn edit_manifest( + ctx: CliContext, + EditManifestParams { expression }: EditManifestParams, + S9pkPath { s9pk: s9pk_path }: S9pkPath, +) -> Result { + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + let old = serde_json::to_value(s9pk.as_manifest()).with_kind(ErrorKind::Serialization)?; + *s9pk.as_manifest_mut() = serde_json::from_value(apply_expr(old.into(), &expression)?.into()) + .with_kind(ErrorKind::Serialization)?; + let manifest = s9pk.as_manifest().clone(); + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + let mut tmp_file = create_file(&tmp_path).await?; + s9pk.as_archive_mut() + .set_signer(ctx.developer_key()?.clone(), SIG_CONTEXT); + s9pk.serialize(&mut tmp_file, true).await?; + tmp_file.sync_all().await?; + tokio::fs::rename(&tmp_path, &s9pk_path).await?; + + Ok(manifest) +} + +async fn file_tree( + ctx: CliContext, + _: Empty, + S9pkPath { s9pk: s9pk_path }: S9pkPath, +) -> Result, Error> { + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + Ok(s9pk.as_archive().contents().file_paths("")) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +struct CatParams { + file_path: PathBuf, +} +async fn cat( + ctx: CliContext, + CatParams { file_path }: CatParams, + S9pkPath { s9pk: s9pk_path }: S9pkPath, +) -> Result<(), Error> { + use crate::s9pk::merkle_archive::source::FileSource; + + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + tokio::io::copy( + &mut s9pk + .as_archive() + .contents() + .get_path(&file_path) + .or_not_found(&file_path.display())? + .as_file() + .or_not_found(&file_path.display())? + .reader() + .await?, + &mut tokio::io::stdout(), + ) + .await?; + Ok(()) +} + +async fn inspect_manifest( + ctx: CliContext, + _: Empty, + S9pkPath { s9pk: s9pk_path }: S9pkPath, +) -> Result { + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + Ok(s9pk.as_manifest().clone()) +} + +async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> { + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + s9pk.serialize(&mut create_file(&tmp_path).await?, true) + .await?; + tokio::fs::rename(tmp_path, s9pk_path).await?; + Ok(()) +} diff --git a/core/startos/src/s9pk/specv2.md b/core/startos/src/s9pk/specv2.md deleted file mode 100644 index 9bf993463..000000000 --- a/core/startos/src/s9pk/specv2.md +++ /dev/null @@ -1,28 +0,0 @@ -## Header - -### Magic - -2B: `0x3b3b` - -### Version - -varint: `0x02` - -### Pubkey - -32B: ed25519 pubkey - -### TOC - -- number of sections (varint) -- FOREACH section - - sig (32B: ed25519 signature of BLAKE-3 of rest of section) - - name (varstring) - - TYPE (varint) - - TYPE=FILE (`0x01`) - - mime (varstring) - - pos (32B: u64 BE) - - len (32B: u64 BE) - - hash (32B: BLAKE-3 of file contents) - - TYPE=TOC (`0x02`) - - recursively defined diff --git a/core/startos/src/s9pk/builder.rs b/core/startos/src/s9pk/v1/builder.rs similarity index 100% rename from core/startos/src/s9pk/builder.rs rename to core/startos/src/s9pk/v1/builder.rs diff --git a/core/startos/src/s9pk/docker.rs b/core/startos/src/s9pk/v1/docker.rs similarity index 57% rename from core/startos/src/s9pk/docker.rs rename to core/startos/src/s9pk/v1/docker.rs index be93905fb..96c532479 100644 --- a/core/startos/src/s9pk/docker.rs +++ b/core/startos/src/s9pk/v1/docker.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::BTreeSet; use std::io::SeekFrom; use std::path::Path; @@ -10,10 +9,10 @@ use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt}; use tokio_tar::{Archive, Entry}; use crate::util::io::from_cbor_async_reader; -use crate::{Error, ErrorKind, ARCH}; +use crate::{Error, ErrorKind}; #[derive(Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct DockerMultiArch { pub default: String, pub available: BTreeSet, @@ -26,8 +25,8 @@ pub enum DockerReader { MultiArch(#[pin] Entry>), } impl DockerReader { - pub async fn new(mut rdr: R) -> Result { - let arch = if let Some(multiarch) = tokio_tar::Archive::new(&mut rdr) + pub async fn list_arches(rdr: &mut R) -> Result, Error> { + if let Some(multiarch) = tokio_tar::Archive::new(rdr) .entries()? .try_filter_map(|e| { async move { @@ -43,41 +42,37 @@ impl DockerReader { .await? { let multiarch: DockerMultiArch = from_cbor_async_reader(multiarch).await?; - Some(if multiarch.available.contains(&**ARCH) { - Cow::Borrowed(&**ARCH) - } else { - Cow::Owned(multiarch.default) - }) + Ok(multiarch.available) } else { - None - }; + Err(Error::new( + eyre!("Single arch legacy s9pks not supported"), + ErrorKind::ParseS9pk, + )) + } + } + pub async fn new(mut rdr: R, arch: &str) -> Result { rdr.seek(SeekFrom::Start(0)).await?; - if let Some(arch) = arch { - if let Some(image) = tokio_tar::Archive::new(rdr) - .entries()? - .try_filter_map(|e| { - let arch = arch.clone(); - async move { - Ok(if &*e.path()? == Path::new(&format!("{}.tar", arch)) { - Some(e) - } else { - None - }) - } - .boxed() - }) - .try_next() - .await? - { - Ok(Self::MultiArch(image)) - } else { - Err(Error::new( - eyre!("Docker image section does not contain tarball for architecture"), - ErrorKind::ParseS9pk, - )) - } + if let Some(image) = tokio_tar::Archive::new(rdr) + .entries()? + .try_filter_map(|e| { + async move { + Ok(if &*e.path()? == Path::new(&format!("{}.tar", arch)) { + Some(e) + } else { + None + }) + } + .boxed() + }) + .try_next() + .await? + { + Ok(Self::MultiArch(image)) } else { - Ok(Self::SingleArch(rdr)) + Err(Error::new( + eyre!("Docker image section does not contain tarball for architecture"), + ErrorKind::ParseS9pk, + )) } } } diff --git a/core/startos/src/s9pk/header.rs b/core/startos/src/s9pk/v1/header.rs similarity index 100% rename from core/startos/src/s9pk/header.rs rename to core/startos/src/s9pk/v1/header.rs diff --git a/core/startos/src/s9pk/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs similarity index 54% rename from core/startos/src/s9pk/manifest.rs rename to core/startos/src/s9pk/v1/manifest.rs index 3eee540ed..31821ad68 100644 --- a/core/startos/src/s9pk/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -1,43 +1,28 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; -use color_eyre::eyre::eyre; +use exver::{Version, VersionRange}; +use imbl_value::InternedString; +use indexmap::IndexMap; pub use models::PackageId; +use models::{ActionId, HealthCheckId, ImageId, VolumeId}; use serde::{Deserialize, Serialize}; use url::Url; -use super::git_hash::GitHash; -use crate::action::Actions; -use crate::backup::BackupActions; -use crate::config::action::ConfigActions; -use crate::dependencies::Dependencies; -use crate::migration::Migrations; -use crate::net::interface::Interfaces; use crate::prelude::*; -use crate::procedure::docker::DockerContainers; -use crate::procedure::PackageProcedure; -use crate::status::health_check::HealthChecks; -use crate::util::serde::Regex; -use crate::util::Version; -use crate::version::{Current, VersionT}; -use crate::volume::Volumes; -use crate::Error; +use crate::s9pk::git_hash::GitHash; +use crate::s9pk::manifest::{Alerts, Description}; +use crate::util::serde::{Duration, IoFormat, Regex}; -fn current_version() -> Version { - Current::new().semver().into() -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -#[model = "Model"] pub struct Manifest { - #[serde(default = "current_version")] pub eos_version: Version, pub id: PackageId, #[serde(default)] pub git_hash: Option, pub title: String, - pub version: Version, + pub version: String, pub description: Description, #[serde(default)] pub assets: Assets, @@ -56,20 +41,19 @@ pub struct Manifest { pub health_checks: HealthChecks, pub config: Option, pub properties: Option, - pub volumes: Volumes, + pub volumes: BTreeMap, // #[serde(default)] - pub interfaces: Interfaces, + // pub interfaces: Interfaces, // #[serde(default)] pub backup: BackupActions, #[serde(default)] pub migrations: Migrations, #[serde(default)] - pub actions: Actions, + pub actions: BTreeMap, // #[serde(default)] // pub permissions: Permissions, #[serde(default)] - pub dependencies: Dependencies, - pub containers: Option, + pub dependencies: BTreeMap, #[serde(default)] pub replaces: Vec, @@ -91,7 +75,7 @@ impl Manifest { .to .values() .chain(self.migrations.from.values()); - let actions = self.actions.0.values().map(|a| &a.implementation); + let actions = self.actions.values().map(|a| &a.implementation); main.chain(cfg_get) .chain(cfg_set) .chain(props) @@ -99,20 +83,123 @@ impl Manifest { .chain(migrations) .chain(actions) } +} - pub fn with_git_hash(mut self, git_hash: GitHash) -> Self { - self.git_hash = Some(git_hash); - self - } +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "type")] +#[model = "Model"] +pub enum PackageProcedure { + Docker(DockerProcedure), + Script(Value), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DockerProcedure { + pub image: ImageId, + #[serde(default)] + pub system: bool, + pub entrypoint: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub inject: bool, + #[serde(default)] + pub mounts: BTreeMap, + #[serde(default)] + pub io_format: Option, + #[serde(default)] + pub sigterm_timeout: Option, + #[serde(default)] + pub shm_size_mb: Option, // TODO: use postfix sizing? like 1k vs 1m vs 1g + #[serde(default)] + pub gpu_acceleration: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HealthChecks(pub BTreeMap); + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct HealthCheck { + pub name: String, + pub success_message: Option, + #[serde(flatten)] + implementation: PackageProcedure, + pub timeout: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConfigActions { + pub get: PackageProcedure, + pub set: PackageProcedure, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BackupActions { + pub create: PackageProcedure, + pub restore: PackageProcedure, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] +pub struct Migrations { + pub from: IndexMap, + pub to: IndexMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Action { + pub name: String, + pub description: String, + #[serde(default)] + pub warning: Option, + pub implementation: PackageProcedure, + // pub allowed_statuses: Vec, + // #[serde(default)] + // pub input_spec: ConfigSpec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DepInfo { + pub version: VersionRange, + pub requirement: DependencyRequirement, + pub description: Option, + #[serde(default)] + pub config: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DependencyConfig { + check: PackageProcedure, + auto_configure: PackageProcedure, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "type")] +pub enum DependencyRequirement { + OptIn { how: String }, + OptOut { how: String }, + Required, +} +impl DependencyRequirement { + pub fn required(&self) -> bool { + matches!(self, &DependencyRequirement::Required) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct HardwareRequirements { #[serde(default)] - device: BTreeMap, - ram: Option, - pub arch: Option>, + pub device: BTreeMap, + pub ram: Option, + pub arch: Option>, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -176,36 +263,3 @@ impl Assets { .unwrap_or(Path::new("scripts")) } } - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Description { - pub short: String, - pub long: String, -} -impl Description { - pub fn validate(&self) -> Result<(), Error> { - if self.short.chars().skip(160).next().is_some() { - return Err(Error::new( - eyre!("Short description must be 160 characters or less."), - crate::ErrorKind::ValidateS9pk, - )); - } - if self.long.chars().skip(5000).next().is_some() { - return Err(Error::new( - eyre!("Long description must be 5000 characters or less."), - crate::ErrorKind::ValidateS9pk, - )); - } - Ok(()) - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Alerts { - pub install: Option, - pub uninstall: Option, - pub restore: Option, - pub start: Option, - pub stop: Option, -} diff --git a/core/startos/src/s9pk/v1/mod.rs b/core/startos/src/s9pk/v1/mod.rs new file mode 100644 index 000000000..9910d0adb --- /dev/null +++ b/core/startos/src/s9pk/v1/mod.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +use clap::Parser; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +pub mod builder; +pub mod docker; +pub mod header; +pub mod manifest; +pub mod reader; + +pub const SIG_CONTEXT: &[u8] = b"s9pk"; + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct VerifyParams { + pub path: PathBuf, +} diff --git a/core/startos/src/s9pk/reader.rs b/core/startos/src/s9pk/v1/reader.rs similarity index 65% rename from core/startos/src/s9pk/reader.rs rename to core/startos/src/s9pk/v1/reader.rs index 61b5e46a8..05f351343 100644 --- a/core/startos/src/s9pk/reader.rs +++ b/core/startos/src/s9pk/v1/reader.rs @@ -10,22 +10,18 @@ use color_eyre::eyre::eyre; use digest::Output; use ed25519_dalek::VerifyingKey; use futures::TryStreamExt; -use models::ImageId; +use models::{ImageId, PackageId}; use sha2::{Digest, Sha512}; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, ReadBuf}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, BufReader, ReadBuf}; use tracing::instrument; use super::header::{FileSection, Header, TableOfContents}; -use super::manifest::{Manifest, PackageId}; use super::SIG_CONTEXT; -use crate::install::progress::InstallProgressTracker; -use crate::s9pk::docker::DockerReader; -use crate::util::Version; -use crate::{Error, ResultExt}; - -const MAX_REPLACES: usize = 10; -const MAX_TITLE_LEN: usize = 30; +use crate::prelude::*; +use crate::s9pk::v1::docker::DockerReader; +use crate::util::io::open_file; +use crate::util::VersionString; #[pin_project::pin_project] #[derive(Debug)] @@ -88,11 +84,11 @@ impl<'a, R: AsyncSeek + Unpin> AsyncSeek for ReadHandle<'a, R> { pub struct ImageTag { pub package_id: PackageId, pub image_id: ImageId, - pub version: Version, + pub version: VersionString, } impl ImageTag { #[instrument(skip_all)] - pub fn validate(&self, id: &PackageId, version: &Version) -> Result<(), Error> { + pub fn validate(&self, id: &PackageId, version: &VersionString) -> Result<(), Error> { if id != &self.package_id { return Err(Error::new( eyre!( @@ -144,7 +140,7 @@ impl FromStr for ImageTag { } } -pub struct S9pkReader { +pub struct S9pkReader> { hash: Option>, hash_string: Option, developer_key: VerifyingKey, @@ -155,120 +151,15 @@ pub struct S9pkReader { impl S9pkReader { pub async fn open>(path: P, check_sig: bool) -> Result { let p = path.as_ref(); - let rdr = File::open(p) - .await - .with_ctx(|_| (crate::error::ErrorKind::Filesystem, p.display().to_string()))?; + let rdr = open_file(p).await?; - Self::from_reader(rdr, check_sig).await - } -} -impl S9pkReader> { - pub fn validated(&mut self) { - self.rdr.validated() + Self::from_reader(BufReader::new(rdr), check_sig).await } } impl S9pkReader { #[instrument(skip_all)] - pub async fn validate(&mut self) -> Result<(), Error> { - if self.toc.icon.length > 102_400 { - // 100 KiB - return Err(Error::new( - eyre!("icon must be less than 100KiB"), - crate::ErrorKind::ValidateS9pk, - )); - } - let image_tags = self.image_tags().await?; - let man = self.manifest().await?; - let containers = &man.containers; - let validated_image_ids = image_tags - .into_iter() - .map(|i| i.validate(&man.id, &man.version).map(|_| i.image_id)) - .collect::, _>>()?; - man.description.validate()?; - man.actions.0.iter().try_for_each(|(_, action)| { - action.validate( - containers, - &man.eos_version, - &man.volumes, - &validated_image_ids, - ) - })?; - man.backup.validate( - containers, - &man.eos_version, - &man.volumes, - &validated_image_ids, - )?; - if let Some(cfg) = &man.config { - cfg.validate( - containers, - &man.eos_version, - &man.volumes, - &validated_image_ids, - )?; - } - man.health_checks - .validate(&man.eos_version, &man.volumes, &validated_image_ids)?; - man.interfaces.validate()?; - man.main - .validate(&man.eos_version, &man.volumes, &validated_image_ids, false) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Main"))?; - man.migrations.validate( - containers, - &man.eos_version, - &man.volumes, - &validated_image_ids, - )?; - - #[cfg(feature = "js-engine")] - if man.containers.is_some() - || matches!(man.main, crate::procedure::PackageProcedure::Script(_)) - { - return Err(Error::new( - eyre!("Right now we don't support the containers and the long running main"), - crate::ErrorKind::ValidateS9pk, - )); - } - - if man.replaces.len() >= MAX_REPLACES { - return Err(Error::new( - eyre!("Cannot have more than {MAX_REPLACES} replaces"), - crate::ErrorKind::ValidateS9pk, - )); - } - if let Some(too_big) = man.replaces.iter().find(|x| x.len() >= MAX_REPLACES) { - return Err(Error::new( - eyre!("We have found a replaces of ({too_big}) that exceeds the max length of {MAX_TITLE_LEN} "), - crate::ErrorKind::ValidateS9pk, - )); - } - if man.title.len() >= MAX_TITLE_LEN { - return Err(Error::new( - eyre!("Cannot have more than a length of {MAX_TITLE_LEN} for title"), - crate::ErrorKind::ValidateS9pk, - )); - } - - if man.containers.is_some() - && matches!(man.main, crate::procedure::PackageProcedure::Docker(_)) - { - return Err(Error::new( - eyre!("Cannot have a main docker and a main in containers"), - crate::ErrorKind::ValidateS9pk, - )); - } - if let Some(props) = &man.properties { - props - .validate(&man.eos_version, &man.volumes, &validated_image_ids, true) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Properties"))?; - } - man.volumes.validate(&man.interfaces)?; - - Ok(()) - } - #[instrument(skip_all)] - pub async fn image_tags(&mut self) -> Result, Error> { - let mut tar = tokio_tar::Archive::new(self.docker_images().await?); + pub async fn image_tags(&mut self, arch: &str) -> Result, Error> { + let mut tar = tokio_tar::Archive::new(self.docker_images(arch).await?); let mut entries = tar.entries()?; while let Some(mut entry) = entries.try_next().await? { if &*entry.path()? != Path::new("manifest.json") { @@ -315,7 +206,7 @@ impl S9pkReader { ( Some(hash), Some(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, hash.as_slice(), )), ) @@ -371,7 +262,7 @@ impl S9pkReader { self.read_handle(self.toc.manifest).await } - pub async fn manifest(&mut self) -> Result { + pub async fn manifest(&mut self) -> Result { let slice = self.manifest_raw().await?.to_vec().await?; serde_cbor::de::from_reader(slice.as_slice()) .with_ctx(|_| (crate::ErrorKind::ParseS9pk, "Deserializing Manifest (CBOR)")) @@ -389,8 +280,15 @@ impl S9pkReader { self.read_handle(self.toc.icon).await } - pub async fn docker_images(&mut self) -> Result>, Error> { - DockerReader::new(self.read_handle(self.toc.docker_images).await?).await + pub async fn docker_arches(&mut self) -> Result, Error> { + DockerReader::list_arches(&mut self.read_handle(self.toc.docker_images).await?).await + } + + pub async fn docker_images( + &mut self, + arch: &str, + ) -> Result>, Error> { + DockerReader::new(self.read_handle(self.toc.docker_images).await?, arch).await } pub async fn assets(&mut self) -> Result, Error> { diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs new file mode 100644 index 000000000..db8cbc414 --- /dev/null +++ b/core/startos/src/s9pk/v2/compat.rs @@ -0,0 +1,270 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +use exver::{ExtendedVersion, VersionRange}; +use models::ImageId; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; +use tokio::process::Command; + +use crate::dependencies::{DepInfo, Dependencies}; +use crate::prelude::*; +use crate::s9pk::manifest::{DeviceFilter, Manifest}; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::TmpSource; +use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; +use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure}; +use crate::s9pk::v1::reader::S9pkReader; +use crate::s9pk::v2::pack::{ImageSource, PackSource, CONTAINER_TOOL}; +use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; +use crate::util::io::{create_file, TmpDir}; +use crate::util::Invoke; + +pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; + +impl S9pk> { + #[instrument(skip_all)] + pub async fn from_v1( + mut reader: S9pkReader, + tmp_dir: Arc, + signer: ed25519_dalek::SigningKey, + ) -> Result { + Command::new(CONTAINER_TOOL) + .arg("run") + .arg("--rm") + .arg("--privileged") + .arg("tonistiigi/binfmt") + .arg("--install") + .arg("all") + .invoke(ErrorKind::Docker) + .await?; + + let mut archive = DirectoryContents::>::new(); + + // manifest.json + let manifest_raw = reader.manifest().await?; + let manifest = from_value::(manifest_raw.clone())?; + let mut new_manifest = Manifest::try_from(manifest.clone())?; + + let images: BTreeSet<(ImageId, bool)> = manifest + .package_procedures() + .filter_map(|p| { + if let PackageProcedure::Docker(p) = p { + Some((p.image.clone(), p.system)) + } else { + None + } + }) + .collect(); + + // LICENSE.md + let license: Arc<[u8]> = reader.license().await?.to_vec().await?.into(); + archive.insert_path( + "LICENSE.md", + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(license.into()), + )), + )?; + + // instructions.md + let instructions: Arc<[u8]> = reader.instructions().await?.to_vec().await?.into(); + archive.insert_path( + "instructions.md", + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(instructions.into()), + )), + )?; + + // icon.md + let icon: Arc<[u8]> = reader.icon().await?.to_vec().await?.into(); + archive.insert_path( + format!("icon.{}", manifest.assets.icon_type()), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(icon.into()), + )), + )?; + + // images + for arch in reader.docker_arches().await? { + Command::new(CONTAINER_TOOL) + .arg("load") + .input(Some(&mut reader.docker_images(&arch).await?)) + .invoke(ErrorKind::Docker) + .await?; + for (image, system) in &images { + let mut image_config = new_manifest.images.remove(image).unwrap_or_default(); + image_config.arch.insert(arch.as_str().into()); + new_manifest.images.insert(image.clone(), image_config); + let image_name = if *system { + format!("start9/{}:latest", image) + } else { + format!("start9/{}/{}:{}", manifest.id, image, manifest.version) + }; + ImageSource::DockerTag(image_name.clone()) + .load( + tmp_dir.clone(), + &new_manifest.id, + &new_manifest.version, + image, + &arch, + &mut archive, + ) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rmi") + .arg("-f") + .arg(&image_name) + .invoke(ErrorKind::Docker) + .await?; + } + } + + // assets + let asset_dir = tmp_dir.join("assets"); + tokio::fs::create_dir_all(&asset_dir).await?; + tokio_tar::Archive::new(reader.assets().await?) + .unpack(&asset_dir) + .await?; + for (asset_id, _) in manifest + .volumes + .iter() + .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets")) + { + let assets_path = asset_dir.join(&asset_id); + let sqfs_path = assets_path.with_extension("squashfs"); + Command::new("mksquashfs") + .arg(&assets_path) + .arg(&sqfs_path) + .invoke(ErrorKind::Filesystem) + .await?; + archive.insert_path( + Path::new("assets") + .join(&asset_id) + .with_extension("squashfs"), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), + )?; + } + + // javascript + let js_dir = tmp_dir.join("javascript"); + let sqfs_path = js_dir.with_extension("squashfs"); + tokio::fs::create_dir_all(&js_dir).await?; + if let Some(mut scripts) = reader.scripts().await? { + let mut js_file = create_file(js_dir.join("embassy.js")).await?; + tokio::io::copy(&mut scripts, &mut js_file).await?; + js_file.sync_all().await?; + } + { + let mut js_file = create_file(js_dir.join("embassyManifest.json")).await?; + js_file + .write_all(&serde_json::to_vec(&manifest_raw).with_kind(ErrorKind::Serialization)?) + .await?; + js_file.sync_all().await?; + } + Command::new("mksquashfs") + .arg(&js_dir) + .arg(&sqfs_path) + .invoke(ErrorKind::Filesystem) + .await?; + archive.insert_path( + Path::new("javascript.squashfs"), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), + )?; + + archive.insert_path( + "manifest.json", + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered( + serde_json::to_vec::(&new_manifest) + .with_kind(ErrorKind::Serialization)? + .into(), + ), + )), + )?; + + let mut res = S9pk::new(MerkleArchive::new(archive, signer, SIG_CONTEXT), None).await?; + res.as_archive_mut().update_hashes(true).await?; + Ok(res) + } +} + +impl TryFrom for Manifest { + type Error = Error; + fn try_from(value: ManifestV1) -> Result { + let default_url = value.upstream_repo.clone(); + Ok(Self { + id: value.id, + title: value.title.into(), + version: ExtendedVersion::from( + exver::emver::Version::from_str(&value.version) + .with_kind(ErrorKind::Deserialization)?, + ) + .into(), + satisfies: BTreeSet::new(), + release_notes: value.release_notes, + can_migrate_from: VersionRange::any(), + can_migrate_to: VersionRange::none(), + license: value.license.into(), + wrapper_repo: value.wrapper_repo, + upstream_repo: value.upstream_repo, + support_site: value.support_site.unwrap_or_else(|| default_url.clone()), + marketing_site: value.marketing_site.unwrap_or_else(|| default_url.clone()), + donation_url: value.donation_url, + description: value.description, + images: BTreeMap::new(), + assets: value + .volumes + .iter() + .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets")) + .map(|(id, _)| id.clone()) + .collect(), + volumes: value + .volumes + .iter() + .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("data")) + .map(|(id, _)| id.clone()) + .collect(), + alerts: value.alerts, + dependencies: Dependencies( + value + .dependencies + .into_iter() + .map(|(id, value)| { + ( + id, + DepInfo { + description: value.description, + optional: !value.requirement.required(), + s9pk: None, + }, + ) + }) + .collect(), + ), + hardware_requirements: super::manifest::HardwareRequirements { + arch: value.hardware_requirements.arch, + ram: value.hardware_requirements.ram, + device: value + .hardware_requirements + .device + .into_iter() + .map(|(class, product)| DeviceFilter { + pattern_description: format!( + "a {class} device matching the expression {}", + product.as_ref() + ), + class, + pattern: product, + }) + .collect(), + }, + git_hash: value.git_hash, + os_version: value.eos_version, + }) + } +} diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs new file mode 100644 index 000000000..11ea0d9af --- /dev/null +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -0,0 +1,214 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; + +use color_eyre::eyre::eyre; +use exver::{Version, VersionRange}; +use imbl_value::InternedString; +pub use models::PackageId; +use models::{mime, ImageId, VolumeId}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::dependencies::Dependencies; +use crate::prelude::*; +use crate::s9pk::git_hash::GitHash; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::expected::{Expected, Filter}; +use crate::s9pk::v2::pack::ImageConfig; +use crate::util::serde::Regex; +use crate::util::VersionString; +use crate::version::{Current, VersionT}; + +fn current_version() -> Version { + Current::default().semver() +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Manifest { + pub id: PackageId, + #[ts(type = "string")] + pub title: InternedString, + pub version: VersionString, + pub satisfies: BTreeSet, + pub release_notes: String, + #[ts(type = "string")] + pub can_migrate_to: VersionRange, + #[ts(type = "string")] + pub can_migrate_from: VersionRange, + #[ts(type = "string")] + pub license: InternedString, // type of license + #[ts(type = "string")] + pub wrapper_repo: Url, + #[ts(type = "string")] + pub upstream_repo: Url, + #[ts(type = "string")] + pub support_site: Url, + #[ts(type = "string")] + pub marketing_site: Url, + #[ts(type = "string | null")] + pub donation_url: Option, + pub description: Description, + pub images: BTreeMap, + pub assets: BTreeSet, // TODO: AssetsId + pub volumes: BTreeSet, + #[serde(default)] + pub alerts: Alerts, + #[serde(default)] + pub dependencies: Dependencies, + #[serde(default)] + pub hardware_requirements: HardwareRequirements, + #[ts(optional)] + #[serde(default = "GitHash::load_sync")] + pub git_hash: Option, + #[serde(default = "current_version")] + #[ts(type = "string")] + pub os_version: Version, +} +impl Manifest { + pub fn validate_for<'a, T: Clone>( + &self, + arch: Option<&str>, + archive: &'a DirectoryContents, + ) -> Result { + let mut expected = Expected::new(archive); + expected.check_file("manifest.json")?; + expected.check_stem("icon", |ext| { + ext.and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |mime| mime.starts_with("image/")) + })?; + expected.check_file("LICENSE.md")?; + expected.check_file("instructions.md")?; + expected.check_file("javascript.squashfs")?; + for (dependency, _) in &self.dependencies.0 { + let dep_path = Path::new("dependencies").join(dependency); + let _ = expected.check_file(dep_path.join("metadata.json")); + let _ = expected.check_stem(dep_path.join("icon"), |ext| { + ext.and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |mime| mime.starts_with("image/")) + }); + } + for assets in &self.assets { + expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; + } + for (image_id, config) in &self.images { + let mut check_arch = |arch: &str| { + let mut arch = arch; + if let Err(e) = expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("squashfs"), + ) { + if let Some(emulate_as) = &config.emulate_missing_as { + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("squashfs"), + )?; + arch = &**emulate_as; + } else { + return Err(e); + } + } + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("json"), + )?; + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("env"), + )?; + Ok(()) + }; + if let Some(arch) = arch { + check_arch(arch)?; + } else if let Some(arches) = &self.hardware_requirements.arch { + for arch in arches { + check_arch(arch)?; + } + } else if let Some(arch) = config.emulate_missing_as.as_deref() { + if !config.arch.contains(arch) { + return Err(Error::new( + eyre!("`emulateMissingAs` must match an included `arch`"), + ErrorKind::ParseS9pk, + )); + } + for arch in &config.arch { + check_arch(&arch)?; + } + } else { + return Err(Error::new(eyre!("`emulateMissingAs` required for all images if no `arch` specified in `hardwareRequirements`"), ErrorKind::ParseS9pk)); + } + } + Ok(expected.into_filter()) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct HardwareRequirements { + #[serde(default)] + pub device: Vec, + #[ts(type = "number | null")] + pub ram: Option, + #[ts(type = "string[] | null")] + pub arch: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct DeviceFilter { + #[ts(type = "\"processor\" | \"display\"")] + pub class: InternedString, + #[ts(type = "string")] + pub pattern: Regex, + pub pattern_description: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct Description { + pub short: String, + pub long: String, +} +impl Description { + pub fn validate(&self) -> Result<(), Error> { + if self.short.chars().skip(160).next().is_some() { + return Err(Error::new( + eyre!("Short description must be 160 characters or less."), + crate::ErrorKind::ValidateS9pk, + )); + } + if self.long.chars().skip(5000).next().is_some() { + return Err(Error::new( + eyre!("Long description must be 5000 characters or less."), + crate::ErrorKind::ValidateS9pk, + )); + } + Ok(()) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct Alerts { + pub install: Option, + pub uninstall: Option, + pub restore: Option, + pub start: Option, + pub stop: Option, +} diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs new file mode 100644 index 000000000..7a94c0d79 --- /dev/null +++ b/core/startos/src/s9pk/v2/mod.rs @@ -0,0 +1,346 @@ +use std::ffi::OsStr; +use std::path::Path; +use std::sync::Arc; + +use imbl_value::InternedString; +use models::{mime, DataUrl, PackageId}; +use tokio::fs::File; + +use crate::dependencies::DependencyMetadata; +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::s9pk::manifest::Manifest; +use crate::s9pk::merkle_archive::sink::Sink; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::{ + ArchiveSource, DynFileSource, FileSource, Section, TmpSource, +}; +use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; +use crate::s9pk::v2::pack::{ImageSource, PackSource}; +use crate::util::io::{open_file, TmpDir}; +use crate::util::serde::IoFormat; + +const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; + +pub const SIG_CONTEXT: &str = "s9pk"; + +pub mod compat; +pub mod manifest; +pub mod pack; + +/** + / + ├── manifest.json + ├── icon. + ├── LICENSE.md + ├── instructions.md + ├── dependencies + │ └── + │ ├── metadata.json + │ └── icon. + ├── javascript.squashfs + ├── assets + │ └── .squashfs (xN) + └── images + └── + ├── .json (xN) + ├── .env (xN) + └── .squashfs (xN) +*/ + +// this sorts the s9pk to optimize such that the parts that are used first appear earlier in the s9pk +// this is useful for manipulating an s9pk while partially downloaded on a source that does not support +// random access +fn priority(s: &str) -> Option { + match s { + "manifest.json" => Some(0), + a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1), + "LICENSE.md" => Some(2), + "instructions.md" => Some(3), + "dependencies" => Some(4), + "javascript.squashfs" => Some(5), + "assets" => Some(6), + "images" => Some(7), + _ => None, + } +} + +#[derive(Clone)] +pub struct S9pk> { + pub manifest: Manifest, + manifest_dirty: bool, + archive: MerkleArchive, + size: Option, +} +impl S9pk { + pub fn as_manifest(&self) -> &Manifest { + &self.manifest + } + pub fn as_manifest_mut(&mut self) -> &mut Manifest { + self.manifest_dirty = true; + &mut self.manifest + } + pub fn as_archive(&self) -> &MerkleArchive { + &self.archive + } + pub fn as_archive_mut(&mut self) -> &mut MerkleArchive { + &mut self.archive + } + pub fn size(&self) -> Option { + self.size + } +} + +impl S9pk { + pub async fn new(archive: MerkleArchive, size: Option) -> Result { + let manifest = extract_manifest(&archive).await?; + Ok(Self { + manifest, + manifest_dirty: false, + archive, + size, + }) + } + + pub fn new_with_manifest( + archive: MerkleArchive, + size: Option, + manifest: Manifest, + ) -> Self { + Self { + manifest, + manifest_dirty: true, + archive, + size, + } + } + + pub fn validate_and_filter(&mut self, arch: Option<&str>) -> Result<(), Error> { + let filter = self.manifest.validate_for(arch, self.archive.contents())?; + filter.keep_checked(self.archive.contents_mut()) + } + + pub async fn icon(&self) -> Result<(InternedString, Entry), Error> { + let mut best_icon = None; + for (path, icon) in self.archive.contents().with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) { + let size = icon.expect_file()?.size().await?; + best_icon = match best_icon { + Some((s, a)) if s >= size => Some((s, a)), + _ => Some((size, (path, icon))), + }; + } + best_icon + .map(|(_, a)| a) + .ok_or_else(|| Error::new(eyre!("no icon found in archive"), ErrorKind::ParseS9pk)) + } + + pub async fn icon_data_url(&self) -> Result, Error> { + let (name, contents) = self.icon().await?; + let mime = Path::new(&*name) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .unwrap_or("image/png"); + Ok(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + )) + } + + pub async fn dependency_icon( + &self, + id: &PackageId, + ) -> Result)>, Error> { + let mut best_icon = None; + for (path, icon) in self + .archive + .contents() + .get_path(Path::new("dependencies").join(id)) + .and_then(|p| p.as_directory()) + .into_iter() + .flat_map(|d| { + d.with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) + }) + { + let size = icon.expect_file()?.size().await?; + best_icon = match best_icon { + Some((s, a)) if s >= size => Some((s, a)), + _ => Some((size, (path, icon))), + }; + } + Ok(best_icon.map(|(_, a)| a)) + } + + pub async fn dependency_icon_data_url( + &self, + id: &PackageId, + ) -> Result>, Error> { + let Some((name, contents)) = self.dependency_icon(id).await? else { + return Ok(None); + }; + let mime = Path::new(&*name) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .unwrap_or("image/png"); + Ok(Some(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + ))) + } + + pub async fn dependency_metadata( + &self, + id: &PackageId, + ) -> Result, Error> { + if let Some(entry) = self + .archive + .contents() + .get_path(Path::new("dependencies").join(id).join("metadata.json")) + { + Ok(Some(IoFormat::Json.from_slice( + &entry.expect_file()?.to_vec(entry.hash()).await?, + )?)) + } else { + Ok(None) + } + } + + pub async fn serialize(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + w.write_all(MAGIC_AND_VERSION).await?; + if !self.manifest_dirty { + self.archive.serialize(w, verify).await?; + } else { + let mut dyn_s9pk = self.clone().into_dyn(); + dyn_s9pk.as_archive_mut().contents_mut().insert_path( + "manifest.json", + Entry::file(DynFileSource::new(Arc::<[u8]>::from( + serde_json::to_vec(&self.manifest).with_kind(ErrorKind::Serialization)?, + ))), + )?; + dyn_s9pk.archive.serialize(w, verify).await?; + } + + Ok(()) + } + + pub fn into_dyn(self) -> S9pk { + S9pk { + manifest: self.manifest, + manifest_dirty: self.manifest_dirty, + archive: self.archive.into_dyn(), + size: self.size, + } + } +} + +impl> + FileSource + Clone> S9pk { + pub async fn load_images(&mut self, tmp_dir: Arc) -> Result<(), Error> { + let id = &self.manifest.id; + let version = &self.manifest.version; + for (image_id, image_config) in &mut self.manifest.images { + self.manifest_dirty = true; + for arch in &image_config.arch { + image_config + .source + .load( + tmp_dir.clone(), + id, + version, + image_id, + arch, + self.archive.contents_mut(), + ) + .await?; + } + image_config.source = ImageSource::Packed; + } + + Ok(()) + } +} + +impl S9pk> { + #[instrument(skip_all)] + pub async fn archive( + source: &S, + commitment: Option<&MerkleArchiveCommitment>, + ) -> Result>, Error> { + use tokio::io::AsyncReadExt; + + let mut header = source + .fetch( + 0, + MAGIC_AND_VERSION.len() as u64 + MerkleArchive::>::header_size(), + ) + .await?; + + let mut magic_version = [0u8; MAGIC_AND_VERSION.len()]; + header.read_exact(&mut magic_version).await?; + ensure_code!( + &magic_version == MAGIC_AND_VERSION, + ErrorKind::ParseS9pk, + "Invalid Magic or Unexpected Version" + ); + MerkleArchive::deserialize(source, SIG_CONTEXT, &mut header, commitment).await + } + #[instrument(skip_all)] + pub async fn deserialize( + source: &S, + commitment: Option<&MerkleArchiveCommitment>, + ) -> Result { + let mut archive = Self::archive(source, commitment).await?; + + archive.sort_by(|a, b| match (priority(a), priority(b)) { + (Some(a), Some(b)) => a.cmp(&b), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + }); + + Self::new(archive, source.size().await).await + } +} +impl S9pk { + pub async fn from_file(file: File) -> Result { + Self::deserialize(&MultiCursorFile::from(file), None).await + } + pub async fn open(path: impl AsRef, id: Option<&PackageId>) -> Result { + let res = Self::from_file(open_file(path).await?).await?; + if let Some(id) = id { + ensure_code!( + &res.as_manifest().id == id, + ErrorKind::ValidateS9pk, + "manifest.id does not match expected" + ); + } + Ok(res) + } +} + +async fn extract_manifest(archive: &MerkleArchive) -> Result { + let manifest = serde_json::from_slice( + &archive + .contents() + .get_path("manifest.json") + .or_not_found("manifest.json")? + .read_file_to_vec() + .await?, + ) + .with_kind(ErrorKind::Deserialization)?; + Ok(manifest) +} diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs new file mode 100644 index 000000000..be81d9e78 --- /dev/null +++ b/core/startos/src/s9pk/v2/pack.rs @@ -0,0 +1,816 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use futures::future::{ready, BoxFuture}; +use futures::{FutureExt, TryStreamExt}; +use imbl_value::InternedString; +use models::{ImageId, PackageId, VersionString}; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; +use tokio::sync::OnceCell; +use tokio_stream::wrappers::ReadDirStream; +use tracing::{debug, warn}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::dependencies::DependencyMetadata; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::s9pk::manifest::Manifest; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::{ + into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, +}; +use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::s9pk::S9pk; +use crate::util::io::{create_file, open_file, TmpDir}; +use crate::util::serde::IoFormat; +use crate::util::{new_guid, Invoke, PathOrUrl}; + +#[cfg(not(feature = "docker"))] +pub const CONTAINER_TOOL: &str = "podman"; +#[cfg(feature = "docker")] +pub const CONTAINER_TOOL: &str = "docker"; + +#[cfg(feature = "docker")] +pub const CONTAINER_DATADIR: &str = "/var/lib/docker"; +#[cfg(not(feature = "docker"))] +pub const CONTAINER_DATADIR: &str = "/var/lib/containers"; + +pub struct SqfsDir { + path: PathBuf, + tmpdir: Arc, + sqfs: OnceCell, +} +impl SqfsDir { + pub fn new(path: PathBuf, tmpdir: Arc) -> Self { + Self { + path, + tmpdir, + sqfs: OnceCell::new(), + } + } + async fn file(&self) -> Result<&MultiCursorFile, Error> { + self.sqfs + .get_or_try_init(|| async move { + let guid = Guid::new(); + let path = self.tmpdir.join(guid.as_ref()).with_extension("squashfs"); + if self.path.extension().and_then(|s| s.to_str()) == Some("tar") { + tar2sqfs(&self.path)? + .input(Some(&mut open_file(&self.path).await?)) + .invoke(ErrorKind::Filesystem) + .await?; + } else { + Command::new("mksquashfs") + .arg(&self.path) + .arg(&path) + .arg("-quiet") + .invoke(ErrorKind::Filesystem) + .await?; + } + + Ok(MultiCursorFile::from( + open_file(&path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, path.display()))?, + )) + }) + .await + } +} + +#[derive(Clone)] +pub enum PackSource { + Buffered(Arc<[u8]>), + File(PathBuf), + Squashfs(Arc), +} +impl FileSource for PackSource { + type Reader = DynRead; + type SliceReader = DynRead; + async fn size(&self) -> Result { + match self { + Self::Buffered(a) => Ok(a.len() as u64), + Self::File(f) => Ok(tokio::fs::metadata(f) + .await + .with_ctx(|_| (ErrorKind::Filesystem, f.display()))? + .len()), + Self::Squashfs(dir) => dir + .file() + .await + .with_ctx(|_| (ErrorKind::Filesystem, dir.path.display()))? + .size() + .await + .or_not_found("file metadata"), + } + } + async fn reader(&self) -> Result { + match self { + Self::Buffered(a) => Ok(into_dyn_read(FileSource::reader(a).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::reader(f).await?)), + Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), + } + } + async fn slice(&self, position: u64, size: u64) -> Result { + match self { + Self::Buffered(a) => Ok(into_dyn_read(FileSource::slice(a, position, size).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::slice(f, position, size).await?)), + Self::Squashfs(dir) => dir + .file() + .await? + .fetch(position, size) + .await + .map(into_dyn_read), + } + } +} +impl From for DynFileSource { + fn from(value: PackSource) -> Self { + DynFileSource::new(value) + } +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct PackParams { + pub path: Option, + #[arg(short = 'o', long = "output")] + pub output: Option, + #[arg(long = "javascript")] + pub javascript: Option, + #[arg(long = "icon")] + pub icon: Option, + #[arg(long = "license")] + pub license: Option, + #[arg(long = "instructions")] + pub instructions: Option, + #[arg(long = "assets")] + pub assets: Option, +} +impl PackParams { + fn path(&self) -> &Path { + self.path.as_deref().unwrap_or(Path::new(".")) + } + fn output(&self, id: &PackageId) -> PathBuf { + self.output + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join(id).with_extension("s9pk")) + } + fn javascript(&self) -> PathBuf { + self.javascript + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("javascript")) + } + async fn icon(&self) -> Result { + if let Some(icon) = &self.icon { + Ok(icon.clone()) + } else { + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")), + ) + }) + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? + } + } + async fn license(&self) -> Result { + if let Some(license) = &self.license { + Ok(license.clone()) + } else { + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")), + ) + }) + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? + } + } + fn instructions(&self) -> PathBuf { + self.instructions + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("instructions.md")) + } + fn assets(&self) -> PathBuf { + self.assets + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("assets")) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ImageConfig { + pub source: ImageSource, + #[ts(type = "string[]")] + pub arch: BTreeSet, + #[ts(type = "string | null")] + pub emulate_missing_as: Option, +} +impl Default for ImageConfig { + fn default() -> Self { + Self { + source: ImageSource::Packed, + arch: BTreeSet::new(), + emulate_missing_as: None, + } + } +} + +#[derive(Parser)] +struct CliImageConfig { + #[arg(long, conflicts_with("docker-tag"))] + docker_build: bool, + #[arg(long, requires("docker-build"))] + dockerfile: Option, + #[arg(long, requires("docker-build"))] + workdir: Option, + #[arg(long, conflicts_with_all(["dockerfile", "workdir"]))] + docker_tag: Option, + #[arg(long)] + arch: Vec, + #[arg(long)] + emulate_missing_as: Option, +} +impl TryFrom for ImageConfig { + type Error = clap::Error; + fn try_from(value: CliImageConfig) -> Result { + let res = Self { + source: if value.docker_build { + ImageSource::DockerBuild { + dockerfile: value.dockerfile, + workdir: value.workdir, + build_args: None, + } + } else if let Some(tag) = value.docker_tag { + ImageSource::DockerTag(tag) + } else { + ImageSource::Packed + }, + arch: value.arch.into_iter().collect(), + emulate_missing_as: value.emulate_missing_as, + }; + res.emulate_missing_as + .as_ref() + .map(|a| { + if !res.arch.contains(a) { + Err(clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + "`emulate-missing-as` must match one of the provided `arch`es", + )) + } else { + Ok(()) + } + }) + .transpose()?; + Ok(res) + } +} +impl clap::Args for ImageConfig { + fn augment_args(cmd: clap::Command) -> clap::Command { + CliImageConfig::augment_args(cmd) + } + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + CliImageConfig::augment_args_for_update(cmd) + } +} +impl clap::FromArgMatches for ImageConfig { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + Self::try_from(CliImageConfig::from_arg_matches(matches)?) + } + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + *self = Self::try_from(CliImageConfig::from_arg_matches(matches)?)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +#[ts(export)] +pub enum BuildArg { + String(String), + EnvVar { env: String }, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum ImageSource { + Packed, + #[serde(rename_all = "camelCase")] + DockerBuild { + #[ts(optional)] + workdir: Option, + #[ts(optional)] + dockerfile: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + build_args: Option>, + }, + DockerTag(String), +} +impl ImageSource { + pub fn ingredients(&self) -> Vec { + match self { + Self::Packed => Vec::new(), + Self::DockerBuild { + dockerfile, + workdir, + .. + } => { + vec![workdir + .as_deref() + .unwrap_or(Path::new(".")) + .join(dockerfile.as_deref().unwrap_or(Path::new("Dockerfile")))] + } + Self::DockerTag(_) => Vec::new(), + } + } + #[instrument(skip_all)] + pub fn load<'a, S: From> + FileSource + Clone>( + &'a self, + tmp_dir: Arc, + id: &'a PackageId, + version: &'a VersionString, + image_id: &'a ImageId, + arch: &'a str, + into: &'a mut DirectoryContents, + ) -> BoxFuture<'a, Result<(), Error>> { + #[derive(Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerImageConfig { + env: Vec, + #[serde(default)] + working_dir: PathBuf, + #[serde(default)] + user: String, + } + async move { + match self { + ImageSource::Packed => Ok(()), + ImageSource::DockerBuild { + workdir, + dockerfile, + build_args, + } => { + let workdir = workdir.as_deref().unwrap_or(Path::new(".")); + let dockerfile = dockerfile + .clone() + .unwrap_or_else(|| workdir.join("Dockerfile")); + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() + } else { + format!("--platform=linux/{arch}") + }; + // docker buildx build ${path} -o type=image,name=start9/${id} + let tag = format!("start9/{id}/{image_id}:{}", new_guid()); + let mut command = Command::new(CONTAINER_TOOL); + command + .arg("build") + .arg(workdir) + .arg("-f") + .arg(dockerfile) + .arg("-t") + .arg(&tag) + .arg(&docker_platform) + .arg("--build-arg") + .arg(format!("ARCH={}", arch)); + + // add build arguments + if let Some(build_args) = build_args { + for (key, value) in build_args { + let build_arg_value = match value { + BuildArg::String(val) => val.to_string(), + BuildArg::EnvVar { env } => { + match std::env::var(&env) { + Ok(val) => val, + Err(_) => continue, // skip if env var not set or invalid + } + } + }; + + command + .arg("--build-arg") + .arg(format!("{}={}", key, build_arg_value)); + } + } + + command + .arg("-o") + .arg("type=docker,dest=-") + .capture(false) + .pipe(Command::new(CONTAINER_TOOL).arg("load")) + .invoke(ErrorKind::Docker) + .await?; + ImageSource::DockerTag(tag.clone()) + .load(tmp_dir, id, version, image_id, arch, into) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rmi") + .arg("-f") + .arg(&tag) + .invoke(ErrorKind::Docker) + .await?; + Ok(()) + } + ImageSource::DockerTag(tag) => { + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() + } else { + format!("--platform=linux/{arch}") + }; + let mut inspect_cmd = Command::new(CONTAINER_TOOL); + inspect_cmd + .arg("image") + .arg("inspect") + .arg("--format") + .arg("{{json .Config}}") + .arg(&tag); + let inspect_res = match inspect_cmd.invoke(ErrorKind::Docker).await { + Ok(a) => a, + Err(e) + if { + let msg = e.source.to_string(); + #[cfg(feature = "docker")] + let matches = msg.contains("No such image:"); + #[cfg(not(feature = "docker"))] + let matches = msg.contains(": image not known"); + matches + } => + { + Command::new(CONTAINER_TOOL) + .arg("pull") + .arg(&docker_platform) + .arg(tag) + .capture(false) + .invoke(ErrorKind::Docker) + .await?; + inspect_cmd.invoke(ErrorKind::Docker).await? + } + Err(e) => return Err(e), + }; + let config = serde_json::from_slice::(&inspect_res) + .with_kind(ErrorKind::Deserialization)?; + let base_path = Path::new("images").join(arch).join(image_id); + into.insert_path( + base_path.with_extension("json"), + Entry::file( + TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered( + serde_json::to_vec(&ImageMetadata { + workdir: if config.working_dir == Path::new("") { + "/".into() + } else { + config.working_dir + }, + user: if config.user.is_empty() { + "root".into() + } else { + config.user.into() + }, + }) + .with_kind(ErrorKind::Serialization)? + .into(), + ), + ) + .into(), + ), + )?; + into.insert_path( + base_path.with_extension("env"), + Entry::file( + TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(config.env.join("\n").into_bytes().into()), + ) + .into(), + ), + )?; + let dest = tmp_dir + .join(Guid::new().as_ref()) + .with_extension("squashfs"); + let container = String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("create") + .arg(&docker_platform) + .arg(&tag) + .invoke(ErrorKind::Docker) + .await?, + )?; + Command::new(CONTAINER_TOOL) + .arg("export") + .arg(container.trim()) + .pipe(&mut tar2sqfs(&dest)?) + .capture(false) + .invoke(ErrorKind::Docker) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rm") + .arg(container.trim()) + .invoke(ErrorKind::Docker) + .await?; + into.insert_path( + base_path.with_extension("squashfs"), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(dest)).into()), + )?; + + Ok(()) + } + } + } + .boxed() + } +} + +fn tar2sqfs(dest: impl AsRef) -> Result { + let dest = dest.as_ref(); + + Ok({ + #[cfg(target_os = "linux")] + { + let mut command = Command::new("tar2sqfs"); + command.arg("-q").arg(&dest); + command + } + #[cfg(target_os = "macos")] + { + let directory = dest + .parent() + .unwrap_or_else(|| Path::new("/")) + .to_path_buf(); + let mut command = Command::new(CONTAINER_TOOL); + command + .arg("run") + .arg("-i") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/data:rw", directory.display())) + .arg("ghcr.io/start9labs/sdk/utils:latest") + .arg("tar2sqfs") + .arg("-q") + .arg(Path::new("/data").join(&dest.file_name().unwrap_or_default())); + command + } + }) +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ImageMetadata { + pub workdir: PathBuf, + #[ts(type = "string")] + pub user: InternedString, +} + +#[instrument(skip_all)] +pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { + let tmp_dir = Arc::new(TmpDir::new().await?); + let mut files = DirectoryContents::>::new(); + let js_dir = params.javascript(); + let manifest: Arc<[u8]> = Command::new("node") + .arg("-e") + .arg(format!( + "console.log(JSON.stringify(require('{}/index.js').manifest))", + js_dir.display() + )) + .invoke(ErrorKind::Javascript) + .await? + .into(); + files.insert( + "manifest.json".into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(manifest.clone()), + )), + ); + let icon = params.icon().await?; + let icon_ext = icon + .extension() + .or_not_found("icon file extension")? + .to_string_lossy(); + files.insert( + InternedString::from_display(&lazy_format!("icon.{}", icon_ext)), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(icon))), + ); + files.insert( + "LICENSE.md".into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::File(params.license().await?), + )), + ); + files.insert( + "instructions.md".into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::File(params.instructions()), + )), + ); + files.insert( + "javascript.squashfs".into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Squashfs(Arc::new(SqfsDir::new(js_dir, tmp_dir.clone()))), + )), + ); + + let mut s9pk = S9pk::new( + MerkleArchive::new(files, ctx.developer_key()?.clone(), SIG_CONTEXT), + None, + ) + .await?; + + let assets_dir = params.assets(); + for assets in s9pk.as_manifest().assets.clone() { + s9pk.as_archive_mut().contents_mut().insert_path( + Path::new("assets").join(&assets).with_extension("squashfs"), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Squashfs(Arc::new(SqfsDir::new( + assets_dir.join(&assets), + tmp_dir.clone(), + ))), + )), + )?; + } + + s9pk.load_images(tmp_dir.clone()).await?; + + let mut to_insert = Vec::new(); + for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 { + if let Some(s9pk) = dependency.s9pk.take() { + let s9pk = match s9pk { + PathOrUrl::Path(path) => { + S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None) + .await? + .into_dyn() + } + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + None, + ) + .await? + .into_dyn() + } else { + return Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )); + } + } + }; + let dep_path = Path::new("dependencies").join(id); + to_insert.push(( + dep_path.join("metadata.json"), + Entry::file(PackSource::Buffered( + IoFormat::Json + .to_vec(&DependencyMetadata { + title: s9pk.as_manifest().title.clone(), + })? + .into(), + )), + )); + let icon = s9pk.icon().await?; + to_insert.push(( + dep_path.join(&*icon.0), + Entry::file(PackSource::Buffered( + icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(), + )), + )); + } else { + warn!("no s9pk specified for {id}, leaving metadata empty"); + } + } + + s9pk.validate_and_filter(None)?; + + s9pk.serialize( + &mut create_file(params.output(&s9pk.as_manifest().id)).await?, + false, + ) + .await?; + + drop(s9pk); + + tmp_dir.gc().await?; + + Ok(()) +} + +#[instrument(skip_all)] +pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result, Error> { + let js_path = params.javascript().join("index.js"); + let manifest: Manifest = match async { + serde_json::from_slice( + &Command::new("node") + .arg("-e") + .arg(format!( + "console.log(JSON.stringify(require('{}').manifest))", + js_path.display() + )) + .invoke(ErrorKind::Javascript) + .await?, + ) + .with_kind(ErrorKind::Deserialization) + } + .await + { + Ok(m) => m, + Err(e) => { + warn!("failed to load manifest: {e}"); + debug!("{e:?}"); + return Ok(vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]); + } + }; + let mut ingredients = vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]; + + for (_, dependency) in manifest.dependencies.0 { + if let Some(PathOrUrl::Path(p)) = dependency.s9pk { + ingredients.push(p); + } + } + + let assets_dir = params.assets(); + for assets in manifest.assets { + ingredients.push(assets_dir.join(assets)); + } + + for image in manifest.images.values() { + ingredients.extend(image.source.ingredients()); + } + + Ok(ingredients) +} diff --git a/core/startos/src/s9pk/v2/specv2.md b/core/startos/src/s9pk/v2/specv2.md new file mode 100644 index 000000000..08dc3336e --- /dev/null +++ b/core/startos/src/s9pk/v2/specv2.md @@ -0,0 +1,89 @@ +## Magic + +`0x3b3b` + +## Version + +`0x02` (varint) + +## Merkle Archive + +### Header + +- ed25519 pubkey (32B) +- ed25519 signature of TOC sighash (64B) +- TOC sighash: (32B) +- TOC position: (8B: u64 BE) +- TOC size: (8B: u64 BE) + +### TOC + +- number of entries (varint) +- FOREACH section + - name (varstring) + - hash (32B: BLAKE-3 of file contents / TOC sighash) + - TYPE (1B) + - TYPE=MISSING (`0x00`) + - TYPE=FILE (`0x01`) + - position (8B: u64 BE) + - size (8B: u64 BE) + - TYPE=TOC (`0x02`) + - position (8B: u64 BE) + - size (8B: u64 BE) + +#### SigHash +Hash of TOC with all contents MISSING + +### FILE + +`` + +# Example + +`foo/bar/baz.txt` + +ROOT TOC: + - 1 section + - name: foo + hash: sighash('a) + type: TOC + position: 'a + length: _ + +'a: + - 1 section + - name: bar + hash: sighash('b) + type: TOC + position: 'b + size: _ + +'b: + - 2 sections + - name: baz.txt + hash: hash('c) + type: FILE + position: 'c + length: _ + - name: qux + hash: `` + type: MISSING + +'c: `` + +"foo/" +hash: _ +size: 15b + +"bar.txt" +hash: _ +size: 5b + +`` ( + "baz.txt" + hash: _ + size: 2b +) +`` ("hello") +`` ("hi") + diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs new file mode 100644 index 000000000..4068d5ad8 --- /dev/null +++ b/core/startos/src/service/action.rs @@ -0,0 +1,183 @@ +use std::collections::BTreeMap; +use std::time::Duration; + +use imbl_value::json; +use models::{ActionId, PackageId, ProcedureName, ReplayId}; + +use crate::action::{ActionInput, ActionResult}; +use crate::db::model::package::{ActionRequestCondition, ActionRequestEntry, ActionRequestInput}; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; +use crate::util::serde::is_partial_of; + +pub(super) struct GetActionInput { + id: ActionId, +} +impl Handler for ServiceActor { + type Response = Result, Error>; + fn conflicts_with(_: &GetActionInput) -> ConflictBuilder { + ConflictBuilder::nothing() + } + async fn handle( + &mut self, + id: Guid, + GetActionInput { id: action_id }: GetActionInput, + _: &BackgroundJobQueue, + ) -> Self::Response { + let container = &self.0.persistent_container; + container + .execute::>( + id, + ProcedureName::GetActionInput(action_id), + Value::Null, + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Action) + } +} + +impl Service { + pub async fn get_action_input( + &self, + id: Guid, + action_id: ActionId, + ) -> Result, Error> { + if !self + .seed + .ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(&self.seed.id) + .or_not_found(&self.seed.id)? + .as_actions() + .as_idx(&action_id) + .or_not_found(&action_id)? + .as_has_input() + .de()? + { + return Ok(None); + } + self.actor + .send(id, GetActionInput { id: action_id }) + .await? + } +} + +pub fn update_requested_actions( + requested_actions: &mut BTreeMap, + package_id: &PackageId, + action_id: &ActionId, + input: &Value, + was_run: bool, +) { + requested_actions.retain(|_, v| { + if &v.request.package_id != package_id || &v.request.action_id != action_id { + return true; + } + if let Some(when) = &v.request.when { + match &when.condition { + ActionRequestCondition::InputNotMatches => match &v.request.input { + Some(ActionRequestInput::Partial { value }) => { + if is_partial_of(value, input) { + if when.once { + return !was_run; + } else { + v.active = false; + } + } else { + v.active = true; + } + } + None => { + tracing::error!( + "action request exists in an invalid state {:?}", + v.request + ); + } + }, + } + true + } else { + !was_run + } + }) +} + +pub(super) struct RunAction { + id: ActionId, + input: Value, +} +impl Handler for ServiceActor { + type Response = Result, Error>; + fn conflicts_with(_: &RunAction) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } + async fn handle( + &mut self, + id: Guid, + RunAction { + id: action_id, + input, + }: RunAction, + _: &BackgroundJobQueue, + ) -> Self::Response { + let container = &self.0.persistent_container; + let result = container + .execute::>( + id, + ProcedureName::RunAction(action_id.clone()), + json!({ + "input": input, + }), + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Action)?; + let package_id = &self.0.id; + self.0 + .ctx + .db + .mutate(|db| { + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + pde.as_requested_actions_mut().mutate(|requested_actions| { + Ok(update_requested_actions( + requested_actions, + package_id, + &action_id, + &input, + true, + )) + })?; + } + Ok(()) + }) + .await?; + Ok(result) + } +} + +impl Service { + pub async fn run_action( + &self, + id: Guid, + action_id: ActionId, + input: Value, + ) -> Result, Error> { + self.actor + .send( + id, + RunAction { + id: action_id, + input, + }, + ) + .await? + } +} diff --git a/core/startos/src/service/cli.rs b/core/startos/src/service/cli.rs new file mode 100644 index 000000000..95add37fb --- /dev/null +++ b/core/startos/src/service/cli.rs @@ -0,0 +1,69 @@ +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use imbl_value::Value; +use once_cell::sync::OnceCell; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty}; +use tokio::runtime::Runtime; + +use crate::lxc::HOST_RPC_SERVER_SOCKET; +use crate::service::effects::context::EffectContext; + +#[derive(Debug, Default, Parser)] +pub struct ContainerClientConfig { + #[arg(long = "socket")] + pub socket: Option, +} + +pub struct ContainerCliSeed { + socket: PathBuf, + runtime: OnceCell>, +} + +#[derive(Clone)] +pub struct ContainerCliContext(Arc); +impl ContainerCliContext { + pub fn init(cfg: ContainerClientConfig) -> Self { + Self(Arc::new(ContainerCliSeed { + socket: cfg + .socket + .unwrap_or_else(|| Path::new("/media/startos/rpc").join(HOST_RPC_SERVER_SOCKET)), + runtime: OnceCell::new(), + })) + } +} +impl Context for ContainerCliContext { + fn runtime(&self) -> Option> { + Some( + self.0 + .runtime + .get_or_init(|| { + Arc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ) + }) + .clone(), + ) + } +} + +impl CallRemote for ContainerCliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_socket( + tokio::net::UnixStream::connect(&self.0.socket) + .await + .map_err(|e| RpcError { + data: Some(e.to_string().into()), + ..yajrc::INTERNAL_ERROR + })?, + method, + params, + ) + .await + } +} diff --git a/core/startos/src/service/control.rs b/core/startos/src/service/control.rs new file mode 100644 index 000000000..8b920bb32 --- /dev/null +++ b/core/startos/src/service/control.rs @@ -0,0 +1,53 @@ +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::action::RunAction; +use crate::service::start_stop::StartStop; +use crate::service::transition::TransitionKind; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; + +pub(super) struct Start; +impl Handler for ServiceActor { + type Response = (); + fn conflicts_with(_: &Start) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } + async fn handle(&mut self, _: Guid, _: Start, _: &BackgroundJobQueue) -> Self::Response { + self.0.persistent_container.state.send_modify(|x| { + x.desired_state = StartStop::Start; + }); + self.0.synchronized.notified().await + } +} +impl Service { + pub async fn start(&self, id: Guid) -> Result<(), Error> { + self.actor.send(id, Start).await + } +} + +struct Stop; +impl Handler for ServiceActor { + type Response = (); + fn conflicts_with(_: &Stop) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } + async fn handle(&mut self, _: Guid, _: Stop, _: &BackgroundJobQueue) -> Self::Response { + let mut transition_state = None; + self.0.persistent_container.state.send_modify(|x| { + x.desired_state = StartStop::Stop; + if x.transition_state.as_ref().map(|x| x.kind()) == Some(TransitionKind::Restarting) { + transition_state = std::mem::take(&mut x.transition_state); + } + }); + if let Some(restart) = transition_state { + restart.abort().await; + } + self.0.synchronized.notified().await + } +} +impl Service { + pub async fn stop(&self, id: Guid) -> Result<(), Error> { + self.actor.send(id, Stop).await + } +} diff --git a/core/startos/src/service/effects/action.rs b/core/startos/src/service/effects/action.rs new file mode 100644 index 000000000..5e3605679 --- /dev/null +++ b/core/startos/src/service/effects/action.rs @@ -0,0 +1,315 @@ +use std::collections::BTreeSet; + +use models::{ActionId, PackageId, ReplayId}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::action::{display_action_result, ActionInput, ActionResult}; +use crate::db::model::package::{ + ActionMetadata, ActionRequest, ActionRequestCondition, ActionRequestEntry, ActionRequestTrigger, +}; +use crate::rpc_continuations::Guid; +use crate::service::cli::ContainerCliContext; +use crate::service::effects::prelude::*; +use crate::util::serde::HandlerExtSerde; + +pub fn action_api() -> ParentHandler { + ParentHandler::new() + .subcommand("export", from_fn_async(export_action).no_cli()) + .subcommand( + "clear", + from_fn_async(clear_actions) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "get-input", + from_fn_async(get_action_input) + .with_display_serializable() + .with_call_remote::(), + ) + .subcommand( + "run", + from_fn_async(run_action) + .with_display_serializable() + .with_custom_display_fn(|args, res| Ok(display_action_result(args.params, res))) + .with_call_remote::(), + ) + .subcommand("request", from_fn_async(request_action).no_cli()) + .subcommand( + "clear-requests", + from_fn_async(clear_action_requests) + .no_display() + .with_call_remote::(), + ) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ExportActionParams { + id: ActionId, + metadata: ActionMetadata, +} +pub async fn export_action( + context: EffectContext, + ExportActionParams { id, metadata }: ExportActionParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + context + .seed + .ctx + .db + .mutate(|db| { + let model = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_actions_mut(); + let mut value = model.de()?; + value.insert(id, metadata); + model.ser(&value) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ClearActionsParams { + #[arg(long)] + pub except: Vec, +} + +async fn clear_actions( + context: EffectContext, + ClearActionsParams { except }: ClearActionsParams, +) -> Result<(), Error> { + let except: BTreeSet<_> = except.into_iter().collect(); + let context = context.deref()?; + let package_id = context.seed.id.clone(); + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_actions_mut() + .mutate(|a| Ok(a.retain(|e, _| except.contains(e)))) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetActionInputParams { + #[serde(default)] + #[ts(skip)] + #[arg(skip)] + procedure_id: Guid, + #[ts(optional)] + package_id: Option, + action_id: ActionId, +} +async fn get_action_input( + context: EffectContext, + GetActionInputParams { + procedure_id, + package_id, + action_id, + }: GetActionInputParams, +) -> Result, Error> { + let context = context.deref()?; + + if let Some(package_id) = package_id { + context + .seed + .ctx + .services + .get(&package_id) + .await + .as_ref() + .or_not_found(&package_id)? + .get_action_input(procedure_id, action_id) + .await + } else { + context.get_action_input(procedure_id, action_id).await + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RunActionParams { + #[serde(default)] + #[ts(skip)] + #[arg(skip)] + procedure_id: Guid, + #[ts(optional)] + package_id: Option, + action_id: ActionId, + #[ts(type = "any")] + input: Value, +} +async fn run_action( + context: EffectContext, + RunActionParams { + procedure_id, + package_id, + action_id, + input, + }: RunActionParams, +) -> Result, Error> { + let context = context.deref()?; + + let package_id = package_id.as_ref().unwrap_or(&context.seed.id); + + if package_id != &context.seed.id { + return Err(Error::new( + eyre!("calling actions on other packages is unsupported at this time"), + ErrorKind::InvalidRequest, + )); + context + .seed + .ctx + .services + .get(&package_id) + .await + .as_ref() + .or_not_found(&package_id)? + .run_action(procedure_id, action_id, input) + .await + } else { + context.run_action(procedure_id, action_id, input).await + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RequestActionParams { + #[serde(default)] + #[ts(skip)] + procedure_id: Guid, + replay_id: ReplayId, + #[serde(flatten)] + request: ActionRequest, +} +async fn request_action( + context: EffectContext, + RequestActionParams { + procedure_id, + replay_id, + request, + }: RequestActionParams, +) -> Result<(), Error> { + let context = context.deref()?; + + let src_id = &context.seed.id; + let active = match &request.when { + Some(ActionRequestTrigger { once, condition }) => match condition { + ActionRequestCondition::InputNotMatches => { + let Some(input) = request.input.as_ref() else { + return Err(Error::new( + eyre!("input-not-matches trigger requires input to be specified"), + ErrorKind::InvalidRequest, + )); + }; + if let Some(service) = context + .seed + .ctx + .services + .get(&request.package_id) + .await + .as_ref() + { + let Some(prev) = service + .get_action_input(procedure_id, request.action_id.clone()) + .await? + else { + return Err(Error::new( + eyre!( + "action {} of {} has no input", + request.action_id, + request.package_id + ), + ErrorKind::InvalidRequest, + )); + }; + if input.matches(prev.value.as_ref()) { + if *once { + return Ok(()); + } else { + false + } + } else { + true + } + } else { + true // update when service is installed + } + } + }, + None => true, + }; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(src_id) + .or_not_found(src_id)? + .as_requested_actions_mut() + .insert(&replay_id, &ActionRequestEntry { active, request }) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(type = "{ only: string[] } | { except: string[] }")] +#[ts(export)] +pub struct ClearActionRequestsParams { + #[arg(long, conflicts_with = "except")] + pub only: Option>, + #[arg(long, conflicts_with = "only")] + pub except: Option>, +} + +async fn clear_action_requests( + context: EffectContext, + ClearActionRequestsParams { only, except }: ClearActionRequestsParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + let only = only.map(|only| only.into_iter().collect::>()); + let except = except.map(|except| except.into_iter().collect::>()); + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_requested_actions_mut() + .mutate(|a| { + Ok(a.retain(|e, _| { + only.as_ref().map_or(true, |only| !only.contains(e)) + && except.as_ref().map_or(true, |except| except.contains(e)) + })) + }) + }) + .await?; + Ok(()) +} diff --git a/core/startos/src/service/effects/callbacks.rs b/core/startos/src/service/effects/callbacks.rs new file mode 100644 index 000000000..19946672c --- /dev/null +++ b/core/startos/src/service/effects/callbacks.rs @@ -0,0 +1,350 @@ +use std::cmp::min; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::{Duration, SystemTime}; + +use clap::Parser; +use futures::future::join_all; +use helpers::NonDetachingJoinHandle; +use imbl::{vector, Vector}; +use imbl_value::InternedString; +use models::{HostId, PackageId, ServiceInterfaceId}; +use patch_db::json_ptr::JsonPointer; +use serde::{Deserialize, Serialize}; +use tracing::warn; +use ts_rs::TS; + +use crate::net::ssl::FullchainCertData; +use crate::prelude::*; +use crate::service::effects::context::EffectContext; +use crate::service::effects::net::ssl::Algorithm; +use crate::service::rpc::{CallbackHandle, CallbackId}; +use crate::service::{Service, ServiceActorSeed}; +use crate::util::collections::EqMap; + +#[derive(Default)] +pub struct ServiceCallbacks(Mutex); + +#[derive(Default)] +struct ServiceCallbackMap { + get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec>, + list_service_interfaces: BTreeMap>, + get_system_smtp: Vec, + get_host_info: BTreeMap<(PackageId, HostId), Vec>, + get_ssl_certificate: EqMap< + (BTreeSet, FullchainCertData, Algorithm), + (NonDetachingJoinHandle<()>, Vec), + >, + get_store: BTreeMap>>, + get_status: BTreeMap>, +} + +impl ServiceCallbacks { + fn mutate(&self, f: impl FnOnce(&mut ServiceCallbackMap) -> T) -> T { + let mut this = self.0.lock().unwrap(); + f(&mut *this) + } + + pub fn gc(&self) { + self.mutate(|this| { + this.get_service_interface.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.list_service_interfaces.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_system_smtp + .retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + this.get_host_info.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_ssl_certificate.retain(|_, (_, v)| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_store.retain(|_, v| { + v.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + !v.is_empty() + }); + this.get_status.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + }) + } + + pub(super) fn add_get_service_interface( + &self, + package_id: PackageId, + service_interface_id: ServiceInterfaceId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_service_interface + .entry((package_id, service_interface_id)) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn get_service_interface( + &self, + id: &(PackageId, ServiceInterfaceId), + ) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.get_service_interface.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_list_service_interfaces( + &self, + package_id: PackageId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.list_service_interfaces + .entry(package_id) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn list_service_interfaces(&self, id: &PackageId) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.list_service_interfaces.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_system_smtp(&self, handler: CallbackHandler) { + self.mutate(|this| { + this.get_system_smtp.push(handler); + }) + } + + #[must_use] + pub fn get_system_smtp(&self) -> Option { + self.mutate(|this| { + Some(CallbackHandlers(std::mem::take(&mut this.get_system_smtp))) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_host_info( + &self, + package_id: PackageId, + host_id: HostId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_host_info + .entry((package_id, host_id)) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn get_host_info(&self, id: &(PackageId, HostId)) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.get_host_info.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_ssl_certificate( + &self, + ctx: EffectContext, + hostnames: BTreeSet, + cert: FullchainCertData, + algorithm: Algorithm, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_ssl_certificate + .entry((hostnames.clone(), cert.clone(), algorithm)) + .or_insert_with(|| { + ( + tokio::spawn(async move { + if let Err(e) = async { + loop { + match cert + .expiration() + .ok() + .and_then(|e| e.duration_since(SystemTime::now()).ok()) + { + Some(d) => { + tokio::time::sleep(min(Duration::from_secs(86400), d)) + .await + } + _ => break, + } + } + let Ok(ctx) = ctx.deref() else { + return Ok(()); + }; + + if let Some((_, callbacks)) = + ctx.seed.ctx.callbacks.mutate(|this| { + this.get_ssl_certificate + .remove(&(hostnames, cert, algorithm)) + }) + { + CallbackHandlers(callbacks).call(vector![]).await?; + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!( + "Error in callback handler for getSslCertificate: {e}" + ); + tracing::debug!("{e:?}"); + } + }) + .into(), + Vec::new(), + ) + }) + .1 + .push(handler); + }) + } + pub(super) fn add_get_status(&self, package_id: PackageId, handler: CallbackHandler) { + self.mutate(|this| this.get_status.entry(package_id).or_default().push(handler)) + } + #[must_use] + pub fn get_status(&self, package_id: &PackageId) -> Option { + self.mutate(|this| { + if let Some(watched) = this.get_status.remove(package_id) { + Some(CallbackHandlers(watched)) + } else { + None + } + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_store( + &self, + package_id: PackageId, + path: JsonPointer, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_store + .entry(package_id) + .or_default() + .entry(path) + .or_default() + .push(handler) + }) + } + + #[must_use] + pub fn get_store( + &self, + package_id: &PackageId, + path: &JsonPointer, + ) -> Option { + self.mutate(|this| { + if let Some(watched) = this.get_store.get_mut(package_id) { + let mut res = Vec::new(); + watched.retain(|ptr, cbs| { + if ptr.starts_with(path) || path.starts_with(ptr) { + res.append(cbs); + false + } else { + true + } + }); + Some(CallbackHandlers(res)) + } else { + None + } + .filter(|cb| !cb.0.is_empty()) + }) + } +} + +pub struct CallbackHandler { + handle: CallbackHandle, + seed: Weak, +} +impl CallbackHandler { + pub fn new(service: &Service, handle: CallbackHandle) -> Self { + Self { + handle, + seed: Arc::downgrade(&service.seed), + } + } + pub async fn call(mut self, args: Vector) -> Result<(), Error> { + crate::dbg!(eyre!("callback fired: {}", self.handle.is_active())); + if let Some(seed) = self.seed.upgrade() { + seed.persistent_container + .callback(self.handle.take(), args) + .await?; + } + Ok(()) + } +} +impl Drop for CallbackHandler { + fn drop(&mut self) { + if self.handle.is_active() { + warn!("Callback handler dropped while still active!"); + } + } +} + +pub struct CallbackHandlers(Vec); +impl CallbackHandlers { + pub async fn call(self, args: Vector) -> Result<(), Error> { + let mut err = ErrorCollection::new(); + for res in join_all(self.0.into_iter().map(|cb| cb.call(args.clone()))).await { + err.handle(res); + } + err.into_result() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(type = "{ only: number[] } | { except: number[] }")] +#[ts(export)] +pub struct ClearCallbacksParams { + #[arg(long, conflicts_with = "except")] + pub only: Option>, + #[arg(long, conflicts_with = "only")] + pub except: Option>, +} + +pub(super) fn clear_callbacks( + context: EffectContext, + ClearCallbacksParams { only, except }: ClearCallbacksParams, +) -> Result<(), Error> { + let context = context.deref()?; + let only = only.map(|only| only.into_iter().collect::>()); + let except = except.map(|except| except.into_iter().collect::>()); + context.seed.persistent_container.state.send_modify(|s| { + s.callbacks.retain(|cb| { + only.as_ref().map_or(true, |only| !only.contains(cb)) + && except.as_ref().map_or(true, |except| except.contains(cb)) + }) + }); + context.seed.ctx.callbacks.gc(); + Ok(()) +} diff --git a/core/startos/src/service/effects/context.rs b/core/startos/src/service/effects/context.rs new file mode 100644 index 000000000..b97499332 --- /dev/null +++ b/core/startos/src/service/effects/context.rs @@ -0,0 +1,27 @@ +use std::sync::{Arc, Weak}; + +use rpc_toolkit::Context; + +use crate::prelude::*; +use crate::service::Service; + +#[derive(Clone)] +pub(in crate::service) struct EffectContext(Weak); +impl EffectContext { + pub fn new(service: Weak) -> Self { + Self(service) + } +} +impl Context for EffectContext {} +impl EffectContext { + pub(super) fn deref(&self) -> Result, Error> { + if let Some(seed) = Weak::upgrade(&self.0) { + Ok(seed) + } else { + Err(Error::new( + eyre!("Service has already been destroyed"), + ErrorKind::InvalidRequest, + )) + } + } +} diff --git a/core/startos/src/service/effects/control.rs b/core/startos/src/service/effects/control.rs new file mode 100644 index 000000000..4b9817b77 --- /dev/null +++ b/core/startos/src/service/effects/control.rs @@ -0,0 +1,108 @@ +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use models::{FromStrParser, PackageId}; + +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; +use crate::status::MainStatus; + +pub async fn restart( + context: EffectContext, + ProcedureId { procedure_id }: ProcedureId, +) -> Result<(), Error> { + let context = context.deref()?; + context.restart(procedure_id).await?; + Ok(()) +} + +pub async fn shutdown( + context: EffectContext, + ProcedureId { procedure_id }: ProcedureId, +) -> Result<(), Error> { + let context = context.deref()?; + context.stop(procedure_id).await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetStatusParams { + #[ts(optional)] + pub package_id: Option, + #[ts(optional)] + #[arg(skip)] + pub callback: Option, +} + +pub async fn get_status( + context: EffectContext, + GetStatusParams { + package_id, + callback, + }: GetStatusParams, +) -> Result { + let context = context.deref()?; + let id = package_id.unwrap_or_else(|| context.seed.id.clone()); + let db = context.seed.ctx.db.peek().await; + let status = db + .as_public() + .as_package_data() + .as_idx(&id) + .or_not_found(&id)? + .as_status() + .de()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_status( + id, + super::callbacks::CallbackHandler::new(&context, callback), + ); + } + + Ok(status) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum SetMainStatusStatus { + Running, + Stopped, +} +impl FromStr for SetMainStatusStatus { + type Err = color_eyre::eyre::Report; + fn from_str(s: &str) -> Result { + match s { + "running" => Ok(Self::Running), + "stopped" => Ok(Self::Stopped), + _ => Err(eyre!("unknown status {s}")), + } + } +} +impl ValueParserFactory for SetMainStatusStatus { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetMainStatus { + status: SetMainStatusStatus, +} +pub async fn set_main_status( + context: EffectContext, + SetMainStatus { status }: SetMainStatus, +) -> Result<(), Error> { + let context = context.deref()?; + match status { + SetMainStatusStatus::Running => context.seed.started(), + SetMainStatusStatus::Stopped => context.seed.stopped(), + } + Ok(()) +} diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs new file mode 100644 index 000000000..2a16b4155 --- /dev/null +++ b/core/startos/src/service/effects/dependency.rs @@ -0,0 +1,382 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use exver::VersionRange; +use imbl::OrdMap; +use imbl_value::InternedString; +use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId}; +use patch_db::json_ptr::JsonPointer; +use tokio::process::Command; + +use crate::db::model::package::{ + ActionRequestEntry, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, + ManifestPreference, +}; +use crate::disk::mount::filesystem::bind::Bind; +use crate::disk::mount::filesystem::idmapped::IdMapped; +use crate::disk::mount::filesystem::{FileSystem, MountType}; +use crate::service::effects::prelude::*; +use crate::status::health_check::NamedHealthCheckResult; +use crate::util::Invoke; +use crate::volume::data_dir; +use crate::DATA_DIR; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct MountTarget { + package_id: PackageId, + volume_id: VolumeId, + subpath: Option, + readonly: bool, +} +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct MountParams { + location: PathBuf, + target: MountTarget, +} +pub async fn mount( + context: EffectContext, + MountParams { + location, + target: + MountTarget { + package_id, + volume_id, + subpath, + readonly, + }, + }: MountParams, +) -> Result<(), Error> { + let context = context.deref()?; + let subpath = subpath.unwrap_or_default(); + let subpath = subpath.strip_prefix("/").unwrap_or(&subpath); + let source = data_dir(DATA_DIR, &package_id, &volume_id).join(subpath); + if tokio::fs::metadata(&source).await.is_err() { + tokio::fs::create_dir_all(&source).await?; + } + let location = location.strip_prefix("/").unwrap_or(&location); + let mountpoint = context + .seed + .persistent_container + .lxc_container + .get() + .or_not_found("lxc container")? + .rootfs_dir() + .join(location); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + IdMapped::new(Bind::new(source), 0, 100000, 65536) + .mount( + mountpoint, + if readonly { + MountType::ReadOnly + } else { + MountType::ReadWrite + }, + ) + .await?; + + Ok(()) +} + +pub async fn get_installed_packages(context: EffectContext) -> Result, Error> { + context + .deref()? + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .keys() +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ExposeForDependentsParams { + #[ts(type = "string[]")] + paths: Vec, +} +pub async fn expose_for_dependents( + context: EffectContext, + ExposeForDependentsParams { paths }: ExposeForDependentsParams, +) -> Result<(), Error> { + // TODO + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum DependencyKind { + Exists, + Running, +} +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase", tag = "kind")] +#[serde(rename_all_fields = "camelCase")] +#[ts(export)] +pub enum DependencyRequirement { + Running { + id: PackageId, + health_checks: BTreeSet, + #[ts(type = "string")] + version_range: VersionRange, + }, + Exists { + id: PackageId, + #[ts(type = "string")] + version_range: VersionRange, + }, +} +// filebrowser:exists,bitcoind:running:foo+bar+baz +impl FromStr for DependencyRequirement { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.split_once(':') { + Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { + id: id.parse()?, + version_range: "*".parse()?, // TODO + }), + Some((id, rest)) => { + let health_checks = match rest.split_once(':') { + Some(("r", rest)) | Some(("running", rest)) => rest + .split('+') + .map(|id| id.parse().map_err(Error::from)) + .collect(), + Some((kind, _)) => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + None => match rest { + "r" | "running" => Ok(BTreeSet::new()), + kind => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + }, + }?; + Ok(Self::Running { + id: id.parse()?, + health_checks, + version_range: "*".parse()?, // TODO + }) + } + None => Ok(Self::Running { + id: s.parse()?, + health_checks: BTreeSet::new(), + version_range: "*".parse()?, // TODO + }), + } + } +} +impl ValueParserFactory for DependencyRequirement { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +pub struct SetDependenciesParams { + dependencies: Vec, +} +pub async fn set_dependencies( + context: EffectContext, + SetDependenciesParams { dependencies }: SetDependenciesParams, +) -> Result<(), Error> { + let context = context.deref()?; + let id = &context.seed.id; + + let mut deps = BTreeMap::new(); + for dependency in dependencies { + let (dep_id, kind, version_range) = match dependency { + DependencyRequirement::Exists { id, version_range } => { + (id, CurrentDependencyKind::Exists, version_range) + } + DependencyRequirement::Running { + id, + health_checks, + version_range, + } => ( + id, + CurrentDependencyKind::Running { health_checks }, + version_range, + ), + }; + let info = CurrentDependencyInfo { + title: context + .seed + .persistent_container + .s9pk + .dependency_metadata(&dep_id) + .await? + .map(|m| m.title), + icon: context + .seed + .persistent_container + .s9pk + .dependency_icon_data_url(&dep_id) + .await?, + kind, + version_range, + }; + deps.insert(dep_id, info); + } + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_current_dependencies_mut() + .ser(&CurrentDependencies(deps)) + }) + .await +} + +pub async fn get_dependencies(context: EffectContext) -> Result, Error> { + let context = context.deref()?; + let id = &context.seed.id; + let db = context.seed.ctx.db.peek().await; + let data = db + .as_public() + .as_package_data() + .as_idx(id) + .or_not_found(id)? + .as_current_dependencies() + .de()?; + + Ok(data + .0 + .into_iter() + .map(|(id, current_dependency_info)| { + let CurrentDependencyInfo { + version_range, + kind, + .. + } = current_dependency_info; + match kind { + CurrentDependencyKind::Exists => { + DependencyRequirement::Exists { id, version_range } + } + CurrentDependencyKind::Running { health_checks } => { + DependencyRequirement::Running { + id, + health_checks, + version_range, + } + } + } + }) + .collect()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CheckDependenciesParam { + #[ts(optional)] + package_ids: Option>, +} +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CheckDependenciesResult { + package_id: PackageId, + #[ts(type = "string | null")] + title: Option, + installed_version: Option, + satisfies: BTreeSet, + is_running: bool, + requested_actions: BTreeMap, + #[ts(as = "BTreeMap::")] + health_checks: OrdMap, +} +pub async fn check_dependencies( + context: EffectContext, + CheckDependenciesParam { package_ids }: CheckDependenciesParam, +) -> Result, Error> { + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; + let pde = db + .as_public() + .as_package_data() + .as_idx(&context.seed.id) + .or_not_found(&context.seed.id)?; + let current_dependencies = pde.as_current_dependencies().de()?; + let requested_actions = pde.as_requested_actions().de()?; + let package_dependency_info: Vec<_> = package_ids + .unwrap_or_else(|| current_dependencies.0.keys().cloned().collect()) + .into_iter() + .filter_map(|x| { + let info = current_dependencies.0.get(&x)?; + Some((x, info)) + }) + .collect(); + let mut results = Vec::with_capacity(package_dependency_info.len()); + + for (package_id, dependency_info) in package_dependency_info { + let title = dependency_info.title.clone(); + let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { + let requested_actions = requested_actions + .iter() + .filter(|(_, v)| v.request.package_id == package_id) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + results.push(CheckDependenciesResult { + package_id, + title, + installed_version: None, + satisfies: BTreeSet::new(), + is_running: false, + requested_actions, + health_checks: Default::default(), + }); + continue; + }; + let manifest = package.as_state_info().as_manifest(ManifestPreference::New); + let installed_version = manifest.as_version().de()?.into_version(); + let satisfies = manifest.as_satisfies().de()?; + let installed_version = Some(installed_version.clone().into()); + let is_installed = true; + let status = package.as_status().de()?; + let is_running = if is_installed { + status.running() + } else { + false + }; + let health_checks = status.health().cloned().unwrap_or_default(); + let requested_actions = requested_actions + .iter() + .filter(|(_, v)| v.request.package_id == package_id) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + results.push(CheckDependenciesResult { + package_id, + title, + installed_version, + satisfies, + is_running, + requested_actions, + health_checks, + }); + } + Ok(results) +} diff --git a/core/startos/src/service/effects/health.rs b/core/startos/src/service/effects/health.rs new file mode 100644 index 000000000..c95dea946 --- /dev/null +++ b/core/startos/src/service/effects/health.rs @@ -0,0 +1,45 @@ +use models::HealthCheckId; + +use crate::service::effects::prelude::*; +use crate::status::health_check::NamedHealthCheckResult; +use crate::status::MainStatus; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetHealth { + id: HealthCheckId, + #[serde(flatten)] + result: NamedHealthCheckResult, +} +pub async fn set_health( + context: EffectContext, + SetHealth { id, result }: SetHealth, +) -> Result<(), Error> { + let context = context.deref()?; + + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(move |db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_status_mut() + .mutate(|main| { + match main { + MainStatus::Running { ref mut health, .. } + | MainStatus::Starting { ref mut health } => { + health.insert(id, result); + } + _ => (), + } + Ok(()) + }) + }) + .await?; + Ok(()) +} diff --git a/core/startos/src/service/effects/mod.rs b/core/startos/src/service/effects/mod.rs new file mode 100644 index 000000000..e9df6f9f2 --- /dev/null +++ b/core/startos/src/service/effects/mod.rs @@ -0,0 +1,191 @@ +use rpc_toolkit::{from_fn, from_fn_async, from_fn_blocking, Context, HandlerExt, ParentHandler}; + +use crate::echo; +use crate::prelude::*; +use crate::service::cli::ContainerCliContext; +use crate::service::effects::context::EffectContext; + +mod action; +pub mod callbacks; +pub mod context; +mod control; +mod dependency; +mod health; +mod net; +mod prelude; +mod store; +mod subcontainer; +mod system; + +pub fn handler() -> ParentHandler { + ParentHandler::new() + .subcommand("git-info", from_fn(|_: C| crate::version::git_info())) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + // action + .subcommand("action", action::action_api::()) + // callbacks + .subcommand( + "clear-callbacks", + from_fn(callbacks::clear_callbacks).no_cli(), + ) + // control + .subcommand( + "restart", + from_fn_async(control::restart) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "shutdown", + from_fn_async(control::shutdown) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "set-main-status", + from_fn_async(control::set_main_status) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "get-status", + from_fn_async(control::get_status) + .no_display() + .with_call_remote::(), + ) + // dependency + .subcommand( + "set-dependencies", + from_fn_async(dependency::set_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "get-dependencies", + from_fn_async(dependency::get_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "check-dependencies", + from_fn_async(dependency::check_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand("mount", from_fn_async(dependency::mount).no_cli()) + .subcommand( + "get-installed-packages", + from_fn_async(dependency::get_installed_packages).no_cli(), + ) + .subcommand( + "expose-for-dependents", + from_fn_async(dependency::expose_for_dependents).no_cli(), + ) + // health + .subcommand("set-health", from_fn_async(health::set_health).no_cli()) + // subcontainer + .subcommand( + "subcontainer", + ParentHandler::::new() + .subcommand( + "launch", + from_fn_blocking(subcontainer::launch).no_display(), + ) + .subcommand( + "launch-init", + from_fn_blocking(subcontainer::launch_init).no_display(), + ) + .subcommand("exec", from_fn_blocking(subcontainer::exec).no_display()) + .subcommand( + "exec-command", + from_fn_blocking(subcontainer::exec_command).no_display(), + ) + .subcommand( + "create-fs", + from_fn_async(subcontainer::create_subcontainer_fs) + .with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display()))) + .with_call_remote::(), + ) + .subcommand( + "destroy-fs", + from_fn_async(subcontainer::destroy_subcontainer_fs) + .no_display() + .with_call_remote::(), + ), + ) + // net + .subcommand("bind", from_fn_async(net::bind::bind).no_cli()) + .subcommand( + "get-service-port-forward", + from_fn_async(net::bind::get_service_port_forward).no_cli(), + ) + .subcommand( + "clear-bindings", + from_fn_async(net::bind::clear_bindings).no_cli(), + ) + .subcommand( + "get-host-info", + from_fn_async(net::host::get_host_info).no_cli(), + ) + .subcommand( + "get-container-ip", + from_fn_async(net::info::get_container_ip).no_cli(), + ) + .subcommand( + "export-service-interface", + from_fn_async(net::interface::export_service_interface).no_cli(), + ) + .subcommand( + "get-service-interface", + from_fn_async(net::interface::get_service_interface).no_cli(), + ) + .subcommand( + "list-service-interfaces", + from_fn_async(net::interface::list_service_interfaces).no_cli(), + ) + .subcommand( + "clear-service-interfaces", + from_fn_async(net::interface::clear_service_interfaces).no_cli(), + ) + .subcommand( + "get-ssl-certificate", + from_fn_async(net::ssl::get_ssl_certificate).no_cli(), + ) + .subcommand("get-ssl-key", from_fn_async(net::ssl::get_ssl_key).no_cli()) + // store + .subcommand( + "store", + ParentHandler::::new() + .subcommand("get", from_fn_async(store::get_store).no_cli()) + .subcommand("set", from_fn_async(store::set_store).no_cli()), + ) + .subcommand( + "set-data-version", + from_fn_async(store::set_data_version) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "get-data-version", + from_fn_async(store::get_data_version) + .with_custom_display_fn(|_, v| { + if let Some(v) = v { + println!("{v}") + } else { + println!("N/A") + } + Ok(()) + }) + .with_call_remote::(), + ) + // system + .subcommand( + "get-system-smtp", + from_fn_async(system::get_system_smtp).no_cli(), + ) + + // TODO Callbacks +} diff --git a/core/startos/src/service/effects/net/bind.rs b/core/startos/src/service/effects/net/bind.rs new file mode 100644 index 000000000..2db3eb2ca --- /dev/null +++ b/core/startos/src/service/effects/net/bind.rs @@ -0,0 +1,93 @@ +use models::{HostId, PackageId}; + +use crate::net::host::binding::{BindId, BindOptions, NetInfo}; +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindParams { + id: HostId, + internal_port: u16, + #[serde(flatten)] + options: BindOptions, +} +pub async fn bind( + context: EffectContext, + BindParams { + id, + internal_port, + options, + }: BindParams, +) -> Result<(), Error> { + let context = context.deref()?; + context + .seed + .persistent_container + .net_service + .bind(id, internal_port, options) + .await +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ClearBindingsParams { + #[serde(default)] + pub except: Vec, +} + +pub async fn clear_bindings( + context: EffectContext, + ClearBindingsParams { except }: ClearBindingsParams, +) -> Result<(), Error> { + let context = context.deref()?; + context + .seed + .persistent_container + .net_service + .clear_bindings(except.into_iter().collect()) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct GetServicePortForwardParams { + #[ts(optional)] + package_id: Option, + host_id: HostId, + internal_port: u16, +} +pub async fn get_service_port_forward( + context: EffectContext, + GetServicePortForwardParams { + package_id, + host_id, + internal_port, + }: GetServicePortForwardParams, +) -> Result { + let context = context.deref()?; + + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + Ok(context + .seed + .ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(&package_id) + .or_not_found(&package_id)? + .as_hosts() + .as_idx(&host_id) + .or_not_found(&host_id)? + .as_bindings() + .de()? + .get(&internal_port) + .or_not_found(lazy_format!("binding for port {internal_port}"))? + .net) +} diff --git a/core/startos/src/service/effects/net/host.rs b/core/startos/src/service/effects/net/host.rs new file mode 100644 index 000000000..570d5033d --- /dev/null +++ b/core/startos/src/service/effects/net/host.rs @@ -0,0 +1,48 @@ +use models::{HostId, PackageId}; + +use crate::net::host::Host; +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetHostInfoParams { + host_id: HostId, + #[ts(optional)] + package_id: Option, + #[ts(optional)] + callback: Option, +} +pub async fn get_host_info( + context: EffectContext, + GetHostInfoParams { + host_id, + package_id, + callback, + }: GetHostInfoParams, +) -> Result, Error> { + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + let res = db + .as_public() + .as_package_data() + .as_idx(&package_id) + .and_then(|m| m.as_hosts().as_idx(&host_id)) + .map(|m| m.de()) + .transpose()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_host_info( + package_id, + host_id, + CallbackHandler::new(&context, callback), + ); + } + + Ok(res) +} diff --git a/core/startos/src/service/effects/net/info.rs b/core/startos/src/service/effects/net/info.rs new file mode 100644 index 000000000..fe6623f44 --- /dev/null +++ b/core/startos/src/service/effects/net/info.rs @@ -0,0 +1,8 @@ +use std::net::Ipv4Addr; + +use crate::service::effects::prelude::*; + +pub async fn get_container_ip(context: EffectContext) -> Result { + let context = context.deref()?; + Ok(context.seed.persistent_container.net_service.get_ip().await) +} diff --git a/core/startos/src/service/effects/net/interface.rs b/core/startos/src/service/effects/net/interface.rs new file mode 100644 index 000000000..5de9638c4 --- /dev/null +++ b/core/startos/src/service/effects/net/interface.rs @@ -0,0 +1,192 @@ +use std::collections::BTreeMap; + +use imbl::vector; +use models::{PackageId, ServiceInterfaceId}; + +use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ExportServiceInterfaceParams { + id: ServiceInterfaceId, + name: String, + description: String, + masked: bool, + address_info: AddressInfo, + r#type: ServiceInterfaceType, +} +pub async fn export_service_interface( + context: EffectContext, + ExportServiceInterfaceParams { + id, + name, + description, + masked, + address_info, + r#type, + }: ExportServiceInterfaceParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + + let service_interface = ServiceInterface { + id: id.clone(), + name, + description, + masked, + address_info, + interface_type: r#type, + }; + + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces_mut() + .insert(&id, &service_interface)?; + Ok(()) + }) + .await?; + if let Some(callbacks) = context + .seed + .ctx + .callbacks + .get_service_interface(&(package_id.clone(), id)) + { + callbacks.call(vector![]).await?; + } + if let Some(callbacks) = context + .seed + .ctx + .callbacks + .list_service_interfaces(&package_id) + { + callbacks.call(vector![]).await?; + } + + Ok(()) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetServiceInterfaceParams { + #[ts(optional)] + package_id: Option, + service_interface_id: ServiceInterfaceId, + #[ts(optional)] + callback: Option, +} +pub async fn get_service_interface( + context: EffectContext, + GetServiceInterfaceParams { + package_id, + service_interface_id, + callback, + }: GetServiceInterfaceParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + let db = context.seed.ctx.db.peek().await; + + let interface = db + .as_public() + .as_package_data() + .as_idx(&package_id) + .and_then(|m| m.as_service_interfaces().as_idx(&service_interface_id)) + .map(|m| m.de()) + .transpose()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_service_interface( + package_id, + service_interface_id, + CallbackHandler::new(&context, callback), + ); + } + + Ok(interface) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ListServiceInterfacesParams { + #[ts(optional)] + package_id: Option, + #[ts(optional)] + callback: Option, +} +pub async fn list_service_interfaces( + context: EffectContext, + ListServiceInterfacesParams { + package_id, + callback, + }: ListServiceInterfacesParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + let res = context + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package_id) + .map(|m| m.into_service_interfaces().de()) + .transpose()? + .unwrap_or_default(); + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context + .seed + .ctx + .callbacks + .add_list_service_interfaces(package_id, CallbackHandler::new(&context, callback)); + } + + Ok(res) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ClearServiceInterfacesParams { + pub except: Vec, +} + +pub async fn clear_service_interfaces( + context: EffectContext, + ClearServiceInterfacesParams { except }: ClearServiceInterfacesParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces_mut() + .mutate(|s| Ok(s.retain(|id, _| except.contains(id)))) + }) + .await +} diff --git a/core/startos/src/service/effects/net/mod.rs b/core/startos/src/service/effects/net/mod.rs new file mode 100644 index 000000000..cf13451a6 --- /dev/null +++ b/core/startos/src/service/effects/net/mod.rs @@ -0,0 +1,5 @@ +pub mod bind; +pub mod host; +pub mod info; +pub mod interface; +pub mod ssl; diff --git a/core/startos/src/service/effects/net/ssl.rs b/core/startos/src/service/effects/net/ssl.rs new file mode 100644 index 000000000..66b4fa1e6 --- /dev/null +++ b/core/startos/src/service/effects/net/ssl.rs @@ -0,0 +1,181 @@ +use std::collections::BTreeSet; + +use imbl_value::InternedString; +use itertools::Itertools; +use openssl::pkey::{PKey, Private}; + +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; +use crate::util::serde::Pem; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum Algorithm { + Ecdsa, + Ed25519, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetSslCertificateParams { + #[ts(type = "string[]")] + hostnames: BTreeSet, + #[ts(optional)] + algorithm: Option, //"ecdsa" | "ed25519" + #[ts(optional)] + callback: Option, +} +pub async fn get_ssl_certificate( + ctx: EffectContext, + GetSslCertificateParams { + hostnames, + algorithm, + callback, + }: GetSslCertificateParams, +) -> Result, Error> { + let context = ctx.deref()?; + let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa); + + let cert = context + .seed + .ctx + .db + .mutate(|db| { + let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound); + let entries = db.as_public().as_package_data().as_entries()?; + let packages = entries.iter().map(|(k, _)| k).collect::>(); + let allowed_hostnames = entries + .iter() + .map(|(_, m)| m.as_hosts().as_entries()) + .flatten_ok() + .map_ok(|(_, m)| { + Ok(m.as_onions() + .de()? + .iter() + .map(InternedString::from_display) + .chain(m.as_domains().keys()?) + .collect::>()) + }) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .try_collect::<_, BTreeSet<_>, _>()?; + for hostname in &hostnames { + if let Some(internal) = hostname + .strip_suffix(".embassy") + .or_else(|| hostname.strip_suffix(".startos")) + { + if !packages.contains(internal) { + return Err(errfn(&*hostname)); + } + } else { + if !allowed_hostnames.contains(hostname) { + return Err(errfn(&*hostname)); + } + } + } + db.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let fullchain = match algorithm { + Algorithm::Ecdsa => cert.fullchain_nistp256(), + Algorithm::Ed25519 => cert.fullchain_ed25519(), + }; + + let res = fullchain + .into_iter() + .map(|c| c.to_pem()) + .map_ok(String::from_utf8) + .map(|a| Ok::<_, Error>(a??)) + .try_collect()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_ssl_certificate( + ctx, + hostnames, + cert, + algorithm, + CallbackHandler::new(&context, callback), + ); + } + + Ok(res) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetSslKeyParams { + #[ts(type = "string[]")] + hostnames: BTreeSet, + #[ts(optional)] + algorithm: Option, //"ecdsa" | "ed25519" +} +pub async fn get_ssl_key( + context: EffectContext, + GetSslKeyParams { + hostnames, + algorithm, + }: GetSslKeyParams, +) -> Result>, Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa); + + let cert = context + .seed + .ctx + .db + .mutate(|db| { + let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound); + let allowed_hostnames = db + .as_public() + .as_package_data() + .as_idx(package_id) + .into_iter() + .map(|m| m.as_hosts().as_entries()) + .flatten_ok() + .map_ok(|(_, m)| { + Ok(m.as_onions() + .de()? + .iter() + .map(InternedString::from_display) + .chain(m.as_domains().keys()?) + .collect::>()) + }) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .try_collect::<_, BTreeSet<_>, _>()?; + for hostname in &hostnames { + if let Some(internal) = hostname + .strip_suffix(".embassy") + .or_else(|| hostname.strip_suffix(".startos")) + { + if internal != &**package_id { + return Err(errfn(&*hostname)); + } + } else { + if !allowed_hostnames.contains(hostname) { + return Err(errfn(&*hostname)); + } + } + } + db.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let key = match algorithm { + Algorithm::Ecdsa => cert.leaf.keys.nistp256, + Algorithm::Ed25519 => cert.leaf.keys.ed25519, + }; + + Ok(Pem(key)) +} diff --git a/core/startos/src/service/effects/prelude.rs b/core/startos/src/service/effects/prelude.rs new file mode 100644 index 000000000..2dc848c0c --- /dev/null +++ b/core/startos/src/service/effects/prelude.rs @@ -0,0 +1,16 @@ +pub use clap::Parser; +pub use serde::{Deserialize, Serialize}; +pub use ts_rs::TS; + +pub use crate::prelude::*; +use crate::rpc_continuations::Guid; +pub(super) use crate::service::effects::context::EffectContext; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ProcedureId { + #[serde(default)] + #[arg(default_value_t, long)] + pub procedure_id: Guid, +} diff --git a/core/startos/src/service/effects/store.rs b/core/startos/src/service/effects/store.rs new file mode 100644 index 000000000..39166c333 --- /dev/null +++ b/core/startos/src/service/effects/store.rs @@ -0,0 +1,139 @@ +use imbl::vector; +use imbl_value::json; +use models::{PackageId, VersionString}; +use patch_db::json_ptr::JsonPointer; + +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetStoreParams { + #[ts(optional)] + package_id: Option, + #[ts(type = "string")] + path: JsonPointer, + #[ts(optional)] + callback: Option, +} +pub async fn get_store( + context: EffectContext, + GetStoreParams { + package_id, + path, + callback, + }: GetStoreParams, +) -> Result { + crate::dbg!(&callback); + let context = context.deref()?; + let peeked = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or(context.seed.id.clone()); + let value = peeked + .as_private() + .as_package_stores() + .as_idx(&package_id) + .map(|s| s.de()) + .transpose()? + .unwrap_or_default(); + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_store( + package_id, + path.clone(), + CallbackHandler::new(&context, callback), + ); + } + + Ok(path.get(&value).cloned().unwrap_or_default()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetStoreParams { + #[ts(type = "any")] + value: Value, + #[ts(type = "string")] + path: JsonPointer, +} +pub async fn set_store( + context: EffectContext, + SetStoreParams { value, path }: SetStoreParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + let model = db + .as_private_mut() + .as_package_stores_mut() + .upsert(package_id, || Ok(json!({})))?; + let mut model_value = model.de()?; + if model_value.is_null() { + model_value = json!({}); + } + path.set(&mut model_value, value, true) + .with_kind(ErrorKind::ParseDbField)?; + model.ser(&model_value) + }) + .await?; + + if let Some(callbacks) = context.seed.ctx.callbacks.get_store(package_id, &path) { + callbacks.call(vector![]).await?; + } + + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetDataVersionParams { + #[ts(type = "string")] + version: VersionString, +} +pub async fn set_data_version( + context: EffectContext, + SetDataVersionParams { version }: SetDataVersionParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_data_version_mut() + .ser(&Some(version)) + }) + .await?; + + Ok(()) +} + +pub async fn get_data_version(context: EffectContext) -> Result, Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(package_id) + .or_not_found(package_id)? + .as_data_version() + .de() +} diff --git a/core/startos/src/service/effects/subcontainer/mod.rs b/core/startos/src/service/effects/subcontainer/mod.rs new file mode 100644 index 000000000..943c70dbf --- /dev/null +++ b/core/startos/src/service/effects/subcontainer/mod.rs @@ -0,0 +1,121 @@ +use std::path::{Path, PathBuf}; + +use imbl_value::InternedString; +use models::ImageId; +use tokio::process::Command; + +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::rpc_continuations::Guid; +use crate::service::effects::prelude::*; +use crate::service::persistent_container::Subcontainer; +use crate::util::Invoke; + +#[cfg(feature = "container-runtime")] +mod sync; + +#[cfg(not(feature = "container-runtime"))] +mod sync_dummy; + +pub use sync::*; +#[cfg(not(feature = "container-runtime"))] +use sync_dummy as sync; + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct DestroySubcontainerFsParams { + guid: Guid, +} +#[instrument(skip_all)] +pub async fn destroy_subcontainer_fs( + context: EffectContext, + DestroySubcontainerFsParams { guid }: DestroySubcontainerFsParams, +) -> Result<(), Error> { + let context = context.deref()?; + if let Some(overlay) = context + .seed + .persistent_container + .subcontainers + .lock() + .await + .remove(&guid) + { + overlay.overlay.unmount(true).await?; + } else { + tracing::warn!("Could not find a subcontainer fs to destroy; assumming that it already is destroyed and will be skipping"); + } + Ok(()) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CreateSubcontainerFsParams { + image_id: ImageId, + #[ts(type = "string | null")] + name: Option, +} +#[instrument(skip_all)] +pub async fn create_subcontainer_fs( + context: EffectContext, + CreateSubcontainerFsParams { image_id, name }: CreateSubcontainerFsParams, +) -> Result<(PathBuf, Guid), Error> { + let context = context.deref()?; + if let Some(image) = context + .seed + .persistent_container + .images + .get(&image_id) + .cloned() + { + let guid = Guid::new(); + let rootfs_dir = context + .seed + .persistent_container + .lxc_container + .get() + .ok_or_else(|| { + Error::new( + eyre!("PersistentContainer has been destroyed"), + ErrorKind::Incoherent, + ) + })? + .rootfs_dir(); + let mountpoint = rootfs_dir + .join("media/startos/subcontainers") + .join(guid.as_ref()); + tokio::fs::create_dir_all(&mountpoint).await?; + let container_mountpoint = Path::new("/").join( + mountpoint + .strip_prefix(rootfs_dir) + .with_kind(ErrorKind::Incoherent)?, + ); + tracing::info!("Mounting overlay {guid} for {image_id}"); + let subcontainer_wrapper = Subcontainer { + overlay: OverlayGuard::mount(image, &mountpoint).await?, + name: name + .unwrap_or_else(|| InternedString::intern(format!("subcontainer-{}", image_id))), + image_id: image_id.clone(), + }; + + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(ErrorKind::Filesystem) + .await?; + tracing::info!("Mounted overlay {guid} for {image_id}"); + context + .seed + .persistent_container + .subcontainers + .lock() + .await + .insert(guid.clone(), subcontainer_wrapper); + Ok((container_mountpoint, guid)) + } else { + Err(Error::new( + eyre!("image {image_id} not found in s9pk"), + ErrorKind::NotFound, + )) + } +} diff --git a/core/startos/src/service/effects/subcontainer/sync.rs b/core/startos/src/service/effects/subcontainer/sync.rs new file mode 100644 index 000000000..702f34bbe --- /dev/null +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -0,0 +1,452 @@ +use std::collections::BTreeMap; +use std::ffi::{c_int, OsStr, OsString}; +use std::fs::File; +use std::io::IsTerminal; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::{Command as StdCommand, Stdio}; + +use nix::sched::CloneFlags; +use nix::unistd::Pid; +use signal_hook::consts::signal::*; +use tokio::sync::oneshot; +use tty_spawn::TtySpawn; + +use crate::service::effects::prelude::*; +use crate::service::effects::ContainerCliContext; + +const FWD_SIGNALS: &[c_int] = &[ + SIGABRT, SIGALRM, SIGCONT, SIGHUP, SIGINT, SIGIO, SIGPIPE, SIGPROF, SIGQUIT, SIGTERM, SIGTRAP, + SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, +]; + +struct NSPid(Vec); +impl procfs::FromBufRead for NSPid { + fn from_buf_read(r: R) -> procfs::ProcResult { + for line in r.lines() { + let line = line?; + if let Some(row) = line.trim().strip_prefix("NSpid") { + return Ok(Self( + row.split_ascii_whitespace() + .map(|pid| pid.parse::()) + .collect::, _>>()?, + )); + } + } + Err(procfs::ProcError::Incomplete(None)) + } +} + +fn open_file_read(path: impl AsRef) -> Result { + File::open(&path).with_ctx(|_| { + ( + ErrorKind::Filesystem, + lazy_format!("open r {}", path.as_ref().display()), + ) + }) +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser)] +pub struct ExecParams { + #[arg(long)] + force_tty: bool, + #[arg(short, long)] + env: Option, + #[arg(short, long)] + workdir: Option, + #[arg(short, long)] + user: Option, + chroot: PathBuf, + #[arg(trailing_var_arg = true)] + command: Vec, +} +impl ExecParams { + fn exec(&self) -> Result<(), Error> { + let ExecParams { + env, + workdir, + user, + chroot, + command, + .. + } = self; + let Some(([command], args)) = command.split_at_checked(1) else { + return Err(Error::new( + eyre!("command cannot be empty"), + ErrorKind::InvalidRequest, + )); + }; + let env_string = if let Some(env) = &env { + std::fs::read_to_string(env) + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))? + } else { + Default::default() + }; + let env = env_string + .lines() + .map(|l| l.trim()) + .filter_map(|l| l.split_once("=")) + .collect::>(); + std::os::unix::fs::chroot(chroot) + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?; + let mut cmd = StdCommand::new(command); + cmd.args(args); + for (k, v) in env { + cmd.env(k, v); + } + + if let Some(uid) = user.as_deref().and_then(|u| u.parse::().ok()) { + cmd.uid(uid); + } else if let Some(user) = user { + let (uid, gid) = std::fs::read_to_string("/etc/passwd") + .with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))? + .lines() + .find_map(|l| { + let mut split = l.trim().split(":"); + if user != split.next()? { + return None; + } + split.next(); // throw away x + Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?)) + // uid gid + }) + .or_not_found(lazy_format!("{user} in /etc/passwd"))?; + cmd.uid(uid); + cmd.gid(gid); + }; + if let Some(workdir) = workdir { + cmd.current_dir(workdir); + } else { + cmd.current_dir("/"); + } + Err(cmd.exec().into()) + } +} + +pub fn launch( + _: ContainerCliContext, + ExecParams { + force_tty, + env, + workdir, + user, + chroot, + command, + }: ExecParams, +) -> Result<(), Error> { + if chroot.join("proc/1").exists() { + let ns_id = procfs::process::Process::new_with_root(chroot.join("proc/1")) + .with_ctx(|_| (ErrorKind::Filesystem, "open subcontainer procfs"))? + .namespaces() + .with_ctx(|_| (ErrorKind::Filesystem, "read subcontainer pid 1 ns"))? + .0 + .get(OsStr::new("pid")) + .or_not_found("pid namespace")? + .identifier; + for proc in + procfs::process::all_processes().with_ctx(|_| (ErrorKind::Filesystem, "open procfs"))? + { + let proc = proc.with_ctx(|_| (ErrorKind::Filesystem, "read single process details"))?; + let pid = proc.pid(); + if proc + .namespaces() + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read pid {} ns", pid)))? + .0 + .get(OsStr::new("pid")) + .map_or(false, |ns| ns.identifier == ns_id) + { + let pids = proc.read::("status").with_ctx(|_| { + ( + ErrorKind::Filesystem, + lazy_format!("read pid {} NSpid", pid), + ) + })?; + if pids.0.len() == 2 && pids.0[1] == 1 { + nix::sys::signal::kill(Pid::from_raw(pid), nix::sys::signal::SIGKILL) + .with_ctx(|_| { + ( + ErrorKind::Filesystem, + lazy_format!( + "kill pid {} (determined to be pid 1 in subcontainer)", + pid + ), + ) + })?; + } + } + } + nix::mount::umount(&chroot.join("proc")) + .with_ctx(|_| (ErrorKind::Filesystem, "unmounting subcontainer procfs"))?; + } + + if (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || force_tty + { + let mut cmd = TtySpawn::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("launch-init"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(command.iter()); + nix::sched::unshare(CloneFlags::CLONE_NEWPID) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWIPC) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?; + std::process::exit(cmd.spawn().with_kind(ErrorKind::Filesystem)?); + } + + let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; + let (send_pid, recv_pid) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(pid) = recv_pid.blocking_recv() { + for sig in sig.forever() { + nix::sys::signal::kill( + Pid::from_raw(pid), + Some(nix::sys::signal::Signal::try_from(sig).unwrap()), + ) + .unwrap(); + } + } + }); + let mut cmd = StdCommand::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("launch-init"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(&command); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + let (stdin_send, stdin_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdin) = stdin_recv.blocking_recv() { + std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); + } + }); + let (stdout_send, stdout_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdout) = stdout_recv.blocking_recv() { + std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); + } + }); + let (stderr_send, stderr_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stderr) = stderr_recv.blocking_recv() { + std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); + } + }); + nix::sched::unshare(CloneFlags::CLONE_NEWPID) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWIPC) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?; + let mut child = cmd + .spawn() + .map_err(color_eyre::eyre::Report::msg) + .with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; + send_pid.send(child.id() as i32).unwrap_or_default(); + stdin_send + .send(child.stdin.take().unwrap()) + .unwrap_or_default(); + stdout_send + .send(child.stdout.take().unwrap()) + .unwrap_or_default(); + stderr_send + .send(child.stderr.take().unwrap()) + .unwrap_or_default(); + // TODO: subreaping, signal handling + let exit = child + .wait() + .with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; + if let Some(code) = exit.code() { + nix::mount::umount(&chroot.join("proc")) + .with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?; + std::process::exit(code); + } else if exit.success() { + Ok(()) + } else { + Err(Error::new( + color_eyre::eyre::Report::msg(exit), + ErrorKind::Unknown, + )) + } +} + +pub fn launch_init(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> { + nix::mount::mount( + Some("proc"), + ¶ms.chroot.join("proc"), + Some("proc"), + nix::mount::MsFlags::empty(), + None::<&str>, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "mount procfs"))?; + if params.command.is_empty() { + signal_hook::iterator::Signals::new(signal_hook::consts::TERM_SIGNALS)? + .forever() + .next(); + std::process::exit(0) + } else { + params.exec() + } +} + +pub fn exec( + _: ContainerCliContext, + ExecParams { + force_tty, + env, + workdir, + user, + chroot, + command, + }: ExecParams, +) -> Result<(), Error> { + if (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || force_tty + { + let mut cmd = TtySpawn::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("exec-command"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(command.iter()); + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/pid"))?, + CloneFlags::CLONE_NEWPID, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set pid ns"))?; + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/cgroup"))?, + CloneFlags::CLONE_NEWCGROUP, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set cgroup ns"))?; + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/ipc"))?, + CloneFlags::CLONE_NEWIPC, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?; + std::process::exit(cmd.spawn().with_kind(ErrorKind::Filesystem)?); + } + let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; + let (send_pid, recv_pid) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(pid) = recv_pid.blocking_recv() { + for sig in sig.forever() { + nix::sys::signal::kill( + Pid::from_raw(pid), + Some(nix::sys::signal::Signal::try_from(sig).unwrap()), + ) + .unwrap(); + } + } + }); + let mut cmd = StdCommand::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("exec-command"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(&command); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + let (stdin_send, stdin_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdin) = stdin_recv.blocking_recv() { + std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); + } + }); + let (stdout_send, stdout_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdout) = stdout_recv.blocking_recv() { + std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); + } + }); + let (stderr_send, stderr_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stderr) = stderr_recv.blocking_recv() { + std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); + } + }); + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/pid"))?, + CloneFlags::CLONE_NEWPID, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set pid ns"))?; + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/cgroup"))?, + CloneFlags::CLONE_NEWCGROUP, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set cgroup ns"))?; + nix::sched::setns( + open_file_read(chroot.join("proc/1/ns/ipc"))?, + CloneFlags::CLONE_NEWIPC, + ) + .with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?; + let mut child = cmd + .spawn() + .map_err(color_eyre::eyre::Report::msg) + .with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; + send_pid.send(child.id() as i32).unwrap_or_default(); + stdin_send + .send(child.stdin.take().unwrap()) + .unwrap_or_default(); + stdout_send + .send(child.stdout.take().unwrap()) + .unwrap_or_default(); + stderr_send + .send(child.stderr.take().unwrap()) + .unwrap_or_default(); + let exit = child + .wait() + .with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; + if let Some(code) = exit.code() { + std::process::exit(code); + } else if exit.success() { + Ok(()) + } else { + Err(Error::new( + color_eyre::eyre::Report::msg(exit), + ErrorKind::Unknown, + )) + } +} + +pub fn exec_command(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> { + params.exec() +} diff --git a/core/startos/src/service/effects/subcontainer/sync_dummy.rs b/core/startos/src/service/effects/subcontainer/sync_dummy.rs new file mode 100644 index 000000000..285bdcbc1 --- /dev/null +++ b/core/startos/src/service/effects/subcontainer/sync_dummy.rs @@ -0,0 +1,30 @@ +use crate::service::effects::prelude::*; +use crate::service::effects::ContainerCliContext; + +pub fn launch(_: ContainerCliContext) -> Result<(), Error> { + Err(Error::new( + eyre!("requires feature container-runtime"), + ErrorKind::InvalidRequest, + )) +} + +pub fn launch_init(_: ContainerCliContext) -> Result<(), Error> { + Err(Error::new( + eyre!("requires feature container-runtime"), + ErrorKind::InvalidRequest, + )) +} + +pub fn exec(_: ContainerCliContext) -> Result<(), Error> { + Err(Error::new( + eyre!("requires feature container-runtime"), + ErrorKind::InvalidRequest, + )) +} + +pub fn exec_command(_: ContainerCliContext) -> Result<(), Error> { + Err(Error::new( + eyre!("requires feature container-runtime"), + ErrorKind::InvalidRequest, + )) +} diff --git a/core/startos/src/service/effects/system.rs b/core/startos/src/service/effects/system.rs new file mode 100644 index 000000000..abf0a33c6 --- /dev/null +++ b/core/startos/src/service/effects/system.rs @@ -0,0 +1,39 @@ +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; +use crate::system::SmtpValue; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct GetSystemSmtpParams { + #[arg(skip)] + callback: Option, +} +pub async fn get_system_smtp( + context: EffectContext, + GetSystemSmtpParams { callback }: GetSystemSmtpParams, +) -> Result, Error> { + let context = context.deref()?; + let res = context + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_smtp() + .de()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context + .seed + .ctx + .callbacks + .add_get_system_smtp(CallbackHandler::new(&context, callback)); + } + + Ok(res) +} diff --git a/core/startos/src/service/fake.cert.key b/core/startos/src/service/fake.cert.key new file mode 100644 index 000000000..a4eb56cb7 --- /dev/null +++ b/core/startos/src/service/fake.cert.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINn5jiv9VFgEwdUJsDksSTAjPKwkl2DCmCmumu4D1GnNoAoGCCqGSM49 +AwEHoUQDQgAE5KuqP+Wdn8pzmNMxK2hya6mKj1H0j5b47y97tIXqf5ajTi8koRPl +yao3YcqdtBtN37aw4rVlXVwEJIozZgyiyA== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/core/startos/src/service/fake.cert.pem b/core/startos/src/service/fake.cert.pem new file mode 100644 index 000000000..fdacaff16 --- /dev/null +++ b/core/startos/src/service/fake.cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9DCCAZmgAwIBAgIUIWsFiA8JqIqeUo+Psn91oCQIcdwwCgYIKoZIzj0EAwIw +TzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRowGAYDVQQKDBFTdGFydDkgTGFi +cywgSW5jLjEXMBUGA1UEAwwOZmFrZW5hbWUubG9jYWwwHhcNMjQwMjE0MTk1MTUz +WhcNMjUwMjEzMTk1MTUzWjBPMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xGjAY +BgNVBAoMEVN0YXJ0OSBMYWJzLCBJbmMuMRcwFQYDVQQDDA5mYWtlbmFtZS5sb2Nh +bDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOSrqj/lnZ/Kc5jTMStocmupio9R +9I+W+O8ve7SF6n+Wo04vJKET5cmqN2HKnbQbTd+2sOK1ZV1cBCSKM2YMosijUzBR +MB0GA1UdDgQWBBR+qd4W//H34Eg90yAPjYz3nZK79DAfBgNVHSMEGDAWgBR+qd4W +//H34Eg90yAPjYz3nZK79DAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kA +MEYCIQDNSN9YWkGbntG+nC+NzEyqE9FcvYZ8TaF3sOnthqSVKwIhAM2N+WJG/p4C +cPl4HSPPgDaOIhVZzxSje2ycb7wvFtpH +-----END CERTIFICATE----- \ No newline at end of file diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs new file mode 100644 index 000000000..2a18ea518 --- /dev/null +++ b/core/startos/src/service/mod.rs @@ -0,0 +1,1195 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::OsString; +use std::io::IsTerminal; +use std::ops::Deref; +use std::os::unix::process::ExitStatusExt; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +use axum::extract::ws::WebSocket; +use chrono::{DateTime, Utc}; +use clap::Parser; +use futures::future::BoxFuture; +use futures::stream::FusedStream; +use futures::{SinkExt, StreamExt, TryStreamExt}; +use imbl_value::{json, InternedString}; +use itertools::Itertools; +use models::{ActionId, HostId, ImageId, PackageId, ProcedureName}; +use nix::sys::signal::Signal; +use persistent_container::{PersistentContainer, Subcontainer}; +use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; +use serde::{Deserialize, Serialize}; +use service_actor::ServiceActor; +use start_stop::StartStop; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::process::Command; +use tokio::sync::Notify; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::package::{ + InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, +}; +use crate::disk::mount::guard::GenericMountGuard; +use crate::install::PKG_ARCHIVE_DIR; +use crate::lxc::ContainerId; +use crate::prelude::*; +use crate::progress::{NamedProgress, Progress}; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::s9pk::S9pk; +use crate::service::action::update_requested_actions; +use crate::service::service_map::InstallProgressHandles; +use crate::util::actor::concurrent::ConcurrentActor; +use crate::util::io::{create_file, AsyncReadStream}; +use crate::util::net::WebSocketExt; +use crate::util::serde::{NoOutput, Pem}; +use crate::util::Never; +use crate::volume::data_dir; +use crate::{CAP_1_KiB, DATA_DIR, PACKAGE_DATA}; + +pub mod action; +pub mod cli; +mod control; +pub mod effects; +pub mod persistent_container; +mod rpc; +mod service_actor; +pub mod service_map; +pub mod start_stop; +mod transition; +mod util; + +pub use service_map::ServiceMap; + +pub const HEALTH_CHECK_COOLDOWN_SECONDS: u64 = 15; +pub const HEALTH_CHECK_GRACE_PERIOD_SECONDS: u64 = 5; +pub const SYNC_RETRY_COOLDOWN_SECONDS: u64 = 10; + +pub type Task<'a> = BoxFuture<'a, Result<(), Error>>; + +/// TODO +pub enum BackupReturn { + TODO, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LoadDisposition { + Retry, + Undo, +} + +struct RootCommand(pub String); + +#[derive(Clone, Debug, Serialize, Deserialize, Default, TS)] +pub struct MiB(pub u64); + +impl MiB { + fn new(value: u64) -> Self { + Self(value / 1024 / 1024) + } + fn from_MiB(value: u64) -> Self { + Self(value) + } +} + +impl std::fmt::Display for MiB { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} MiB", self.0) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default, TS)] +pub struct ServiceStats { + pub container_id: Arc, + pub package_id: PackageId, + pub memory_usage: MiB, + pub memory_limit: MiB, +} + +pub struct ServiceRef(Arc); +impl ServiceRef { + pub fn weak(&self) -> Weak { + Arc::downgrade(&self.0) + } + pub async fn uninstall( + self, + target_version: Option, + ) -> Result<(), Error> { + self.seed + .persistent_container + .execute::( + Guid::new(), + ProcedureName::PackageUninit, + to_value(&target_version)?, + None, + ) // TODO timeout + .await?; + let id = self.seed.persistent_container.s9pk.as_manifest().id.clone(); + let ctx = self.seed.ctx.clone(); + self.shutdown().await?; + + if target_version.is_none() { + if let Some(pde) = ctx + .db + .mutate(|d| { + if let Some(pde) = d + .as_public_mut() + .as_package_data_mut() + .remove(&id)? + .map(|d| d.de()) + .transpose()? + { + d.as_private_mut().as_available_ports_mut().mutate(|p| { + p.free( + pde.hosts + .0 + .values() + .flat_map(|h| h.bindings.values()) + .flat_map(|b| { + b.net + .assigned_port + .into_iter() + .chain(b.net.assigned_ssl_port) + }), + ); + Ok(()) + })?; + d.as_private_mut().as_package_stores_mut().remove(&id)?; + Ok(Some(pde)) + } else { + Ok(None) + } + }) + .await? + { + let state = pde.state_info.expect_removing()?; + for volume_id in &state.manifest.volumes { + let path = data_dir(DATA_DIR, &state.manifest.id, volume_id); + if tokio::fs::metadata(&path).await.is_ok() { + tokio::fs::remove_dir_all(&path).await?; + } + } + let logs_dir = Path::new(PACKAGE_DATA) + .join("logs") + .join(&state.manifest.id); + if tokio::fs::metadata(&logs_dir).await.is_ok() { + tokio::fs::remove_dir_all(&logs_dir).await?; + } + let archive_path = Path::new(PACKAGE_DATA) + .join("archive") + .join("installed") + .join(&state.manifest.id); + if tokio::fs::metadata(&archive_path).await.is_ok() { + tokio::fs::remove_file(&archive_path).await?; + } + } + } + Ok(()) + } + pub async fn shutdown(self) -> Result<(), Error> { + if let Some((hdl, shutdown)) = self.seed.persistent_container.rpc_server.send_replace(None) + { + self.seed + .persistent_container + .rpc_client + .request(rpc::Exit, Empty {}) + .await?; + shutdown.shutdown(); + hdl.await.with_kind(ErrorKind::Cancelled)?; + } + let service = Arc::try_unwrap(self.0).map_err(|_| { + Error::new( + eyre!("ServiceActor held somewhere after actor shutdown"), + ErrorKind::Unknown, + ) + })?; + service + .actor + .shutdown(crate::util::actor::PendingMessageStrategy::FinishAll { timeout: None }) // TODO timeout + .await; + Arc::try_unwrap(service.seed) + .map_err(|_| { + Error::new( + eyre!("ServiceActorSeed held somewhere after actor shutdown"), + ErrorKind::Unknown, + ) + })? + .persistent_container + .exit() + .await?; + Ok(()) + } +} +impl Deref for ServiceRef { + type Target = Service; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl From for ServiceRef { + fn from(value: Service) -> Self { + Self(Arc::new(value)) + } +} + +pub struct Service { + actor: ConcurrentActor, + seed: Arc, +} +impl Service { + #[instrument(skip_all)] + async fn new(ctx: RpcContext, s9pk: S9pk, start: StartStop) -> Result { + let id = s9pk.as_manifest().id.clone(); + let persistent_container = PersistentContainer::new( + &ctx, s9pk, + start, + // desired_state.subscribe(), + // temp_desired_state.subscribe(), + ) + .await?; + let seed = Arc::new(ServiceActorSeed { + id, + persistent_container, + ctx, + synchronized: Arc::new(Notify::new()), + }); + let service: ServiceRef = Self { + actor: ConcurrentActor::new(ServiceActor(seed.clone())), + seed, + } + .into(); + service + .seed + .persistent_container + .init(service.weak()) + .await?; + Ok(service) + } + + #[instrument(skip_all)] + pub async fn load( + ctx: &RpcContext, + id: &PackageId, + disposition: LoadDisposition, + ) -> Result, Error> { + let handle_installed = { + let ctx = ctx.clone(); + move |s9pk: S9pk, i: Model| async move { + for volume_id in &s9pk.as_manifest().volumes { + let path = data_dir(DATA_DIR, &s9pk.as_manifest().id, volume_id); + if tokio::fs::metadata(&path).await.is_err() { + tokio::fs::create_dir_all(&path).await?; + } + } + let start_stop = if i.as_status().de()?.running() { + StartStop::Start + } else { + StartStop::Stop + }; + Self::new(ctx, s9pk, start_stop).await.map(Some) + } + }; + let s9pk_dir = Path::new(DATA_DIR).join(PKG_ARCHIVE_DIR).join("installed"); // TODO: make this based on hash + let s9pk_path = s9pk_dir.join(id).with_extension("s9pk"); + let Some(entry) = ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(id) + else { + return Ok(None); + }; + match entry.as_state_info().as_match() { + PackageStateMatchModelRef::Installing(_) => { + if disposition == LoadDisposition::Retry { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { + tracing::error!("Error opening s9pk for install: {e}"); + tracing::debug!("{e:?}") + }) { + if let Ok(service) = + Self::install(ctx.clone(), s9pk, None, None::, None) + .await + .map_err(|e| { + tracing::error!("Error installing service: {e}"); + tracing::debug!("{e:?}") + }) + { + return Ok(Some(service)); + } + } + } + // TODO: delete s9pk? + ctx.db + .mutate(|v| v.as_public_mut().as_package_data_mut().remove(id)) + .await?; + Ok(None) + } + PackageStateMatchModelRef::Updating(s) => { + if disposition == LoadDisposition::Retry + && s.as_installing_info() + .as_progress() + .de()? + .phases + .iter() + .any(|NamedProgress { name, progress }| { + name.eq_ignore_ascii_case("download") + && progress == &Progress::Complete(true) + }) + { + if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { + tracing::error!("Error opening s9pk for update: {e}"); + tracing::debug!("{e:?}") + }) { + if let Ok(service) = Self::install( + ctx.clone(), + s9pk, + Some(s.as_manifest().as_version().de()?), + None::, + None, + ) + .await + .map_err(|e| { + tracing::error!("Error installing service: {e}"); + tracing::debug!("{e:?}") + }) { + return Ok(Some(service)); + } + } + } + let s9pk = S9pk::open(s9pk_path, Some(id)).await?; + ctx.db + .mutate({ + |db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_state_info_mut() + .map_mutate(|s| { + if let PackageState::Updating(UpdatingState { + manifest, .. + }) = s + { + Ok(PackageState::Installed(InstalledState { manifest })) + } else { + Err(Error::new(eyre!("Race condition detected - package state changed during load"), ErrorKind::Database)) + } + }) + } + }) + .await?; + handle_installed(s9pk, entry).await + } + PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { + tracing::error!("Error opening s9pk for removal: {e}"); + tracing::debug!("{e:?}") + }) { + if let Ok(service) = Self::new(ctx.clone(), s9pk, StartStop::Stop) + .await + .map_err(|e| { + tracing::error!("Error loading service for removal: {e}"); + tracing::debug!("{e:?}") + }) + { + match service.uninstall(None).await { + Err(e) => { + tracing::error!("Error uninstalling service: {e}"); + tracing::debug!("{e:?}") + } + Ok(()) => return Ok(None), + } + } + } + + ctx.db + .mutate(|v| v.as_public_mut().as_package_data_mut().remove(id)) + .await?; + + Ok(None) + } + PackageStateMatchModelRef::Installed(_) => { + handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await + } + PackageStateMatchModelRef::Error(e) => Err(Error::new( + eyre!("Failed to parse PackageDataEntry, found {e:?}"), + ErrorKind::Deserialization, + )), + } + } + + #[instrument(skip_all)] + pub async fn install( + ctx: RpcContext, + s9pk: S9pk, + mut src_version: Option, + recovery_source: Option, + progress: Option, + ) -> Result { + let manifest = s9pk.as_manifest().clone(); + let developer_key = s9pk.as_archive().signer(); + let icon = s9pk.icon_data_url().await?; + let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?; + + if let Some(recovery_source) = recovery_source { + service + .actor + .send( + Guid::new(), + transition::restore::Restore { + path: recovery_source.path().to_path_buf(), + }, + ) + .await??; + recovery_source.unmount().await?; + src_version = Some( + service + .seed + .persistent_container + .s9pk + .as_manifest() + .version + .clone(), + ); + } + + let procedure_id = Guid::new(); + service + .seed + .persistent_container + .execute::( + procedure_id.clone(), + ProcedureName::PackageInit, + to_value(&src_version)?, + None, + ) // TODO timeout + .await + .with_kind(ErrorKind::MigrationFailed)?; // TODO: handle cancellation + + if let Some(mut progress) = progress { + progress.finalization_progress.complete(); + progress.progress.complete(); + tokio::task::yield_now().await; + } + + let peek = ctx.db.peek().await; + let mut action_input: BTreeMap = BTreeMap::new(); + let requested_actions: BTreeSet<_> = peek + .as_public() + .as_package_data() + .as_entries()? + .into_iter() + .map(|(_, pde)| { + Ok(pde + .as_requested_actions() + .as_entries()? + .into_iter() + .map(|(_, r)| { + Ok::<_, Error>(if r.as_request().as_package_id().de()? == manifest.id { + Some(r.as_request().as_action_id().de()?) + } else { + None + }) + }) + .filter_map_ok(|a| a)) + }) + .flatten_ok() + .map(|a| a.and_then(|a| a)) + .try_collect()?; + for action_id in requested_actions { + if let Some(input) = service + .get_action_input(procedure_id.clone(), action_id.clone()) + .await? + .and_then(|i| i.value) + { + action_input.insert(action_id, input); + } + } + ctx.db + .mutate(|db| { + for (action_id, input) in &action_input { + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + pde.as_requested_actions_mut().mutate(|requested_actions| { + Ok(update_requested_actions( + requested_actions, + &manifest.id, + action_id, + input, + false, + )) + })?; + } + } + let entry = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&manifest.id) + .or_not_found(&manifest.id)?; + entry + .as_state_info_mut() + .ser(&PackageState::Installed(InstalledState { manifest }))?; + entry.as_developer_key_mut().ser(&Pem::new(developer_key))?; + entry.as_icon_mut().ser(&icon)?; + // TODO: marketplace url + // TODO: dependency info + + Ok(()) + }) + .await?; + + Ok(service) + } + + #[instrument(skip_all)] + pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { + let id = &self.seed.id; + let mut file = create_file(guard.path().join(id).with_extension("s9pk")).await?; + self.seed + .persistent_container + .s9pk + .clone() + .serialize(&mut file, true) + .await?; + drop(file); + self.actor + .send( + Guid::new(), + transition::backup::Backup { + path: guard.path().join("data"), + }, + ) + .await?? + .await?; + Ok(()) + } + + pub fn container_id(&self) -> Result { + let id = &self.seed.id; + let container_id = (*self + .seed + .persistent_container + .lxc_container + .get() + .or_not_found(format!("container for {id}"))? + .guid) + .clone(); + Ok(container_id) + } + #[instrument(skip_all)] + pub async fn stats(&self) -> Result { + let container = &self.seed.persistent_container; + let lxc_container = container.lxc_container.get().or_not_found("container")?; + let (total, used) = lxc_container + .command(&["free", "-m"]) + .await? + .split("\n") + .map(|x| x.split_whitespace().collect::>()) + .skip(1) + .filter_map(|x| { + Some(( + x.get(1)?.parse::().ok()?, + x.get(2)?.parse::().ok()?, + )) + }) + .fold((0, 0), |acc, (total, used)| (acc.0 + total, acc.1 + used)); + Ok(ServiceStats { + container_id: lxc_container.guid.clone(), + package_id: self.seed.id.clone(), + memory_limit: MiB::from_MiB(total), + memory_usage: MiB::from_MiB(used), + }) + } + + pub async fn sync_host(&self, host_id: HostId) -> Result<(), Error> { + self.seed + .persistent_container + .net_service + .sync_host(host_id) + .await + } +} + +#[derive(Debug, Clone)] +pub struct RunningStatus { + started: DateTime, +} + +struct ServiceActorSeed { + ctx: RpcContext, + id: PackageId, + /// Needed to interact with the container for the service + persistent_container: PersistentContainer, + /// This is notified every time the background job created in ServiceActor::init responds to a change + synchronized: Arc, +} + +impl ServiceActorSeed { + /// Used to indicate that we have finished the task of starting the service + pub fn started(&self) { + self.persistent_container.state.send_modify(|state| { + state.running_status = + Some( + state + .running_status + .take() + .unwrap_or_else(|| RunningStatus { + started: Utc::now(), + }), + ); + }); + } + /// Used to indicate that we have finished the task of stopping the service + pub fn stopped(&self) { + self.persistent_container.state.send_modify(|state| { + state.running_status = None; + }); + } +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct RebuildParams { + pub id: PackageId, +} +pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Result<(), Error> { + ctx.services.load(&ctx, &id, LoadDisposition::Retry).await?; + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct ConnectParams { + pub id: PackageId, +} + +pub async fn connect_rpc( + ctx: RpcContext, + ConnectParams { id }: ConnectParams, +) -> Result { + let id_ref = &id; + crate::lxc::connect( + &ctx, + ctx.services + .get(&id) + .await + .as_ref() + .or_not_found(lazy_format!("service for {id_ref}"))? + .seed + .persistent_container + .lxc_container + .get() + .or_not_found(lazy_format!("container for {id_ref}"))?, + ) + .await +} + +pub async fn connect_rpc_cli( + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgs, +) -> Result<(), Error> { + let ctx = context.clone(); + let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: rpc_toolkit::util::Flat(params, Empty {}), + inherited_params, + raw_params, + }) + .await?; + + crate::lxc::connect_cli(&ctx, guid).await +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct AttachParams { + pub id: PackageId, + #[ts(type = "string[]")] + pub command: Vec, + pub tty: bool, + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: Option, + #[ts(type = "string | null")] + subcontainer: Option, + #[ts(type = "string | null")] + name: Option, + #[ts(type = "string | null")] + image_id: Option, +} +pub async fn attach( + ctx: RpcContext, + AttachParams { + id, + command, + tty, + session, + subcontainer, + image_id, + name, + }: AttachParams, +) -> Result { + let (container_id, subcontainer_id, image_id, workdir, root_command) = { + let id = &id; + + let service = ctx.services.get(id).await; + + let service_ref = service.as_ref().or_not_found(id)?; + + let container = &service_ref.seed.persistent_container; + let root_dir = container + .lxc_container + .get() + .map(|x| x.rootfs_dir().to_owned()) + .or_not_found(format!("container for {id}"))?; + + let subcontainer = subcontainer.map(|x| AsRef::::as_ref(&x).to_uppercase()); + let name = name.map(|x| AsRef::::as_ref(&x).to_uppercase()); + let image_id = image_id.map(|x| AsRef::::as_ref(&x).to_string_lossy().to_uppercase()); + + let subcontainers = container.subcontainers.lock().await; + let subcontainer_ids: Vec<_> = subcontainers + .iter() + .filter(|(x, wrapper)| { + if let Some(subcontainer) = subcontainer.as_ref() { + AsRef::::as_ref(x).contains(AsRef::::as_ref(subcontainer)) + } else if let Some(name) = name.as_ref() { + AsRef::::as_ref(&wrapper.name) + .to_uppercase() + .contains(AsRef::::as_ref(name)) + } else if let Some(image_id) = image_id.as_ref() { + let Some(wrapper_image_id) = AsRef::::as_ref(&wrapper.image_id).to_str() + else { + return false; + }; + wrapper_image_id + .to_uppercase() + .contains(AsRef::::as_ref(&image_id)) + } else { + true + } + }) + .collect(); + let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| { + format!( + "{guid} imageId: {image_id} name: \"{name}\"", + name = &wrapper.name, + image_id = &wrapper.image_id + ) + }; + let Some((subcontainer_id, image_id)) = subcontainer_ids + .first() + .map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone())) + else { + drop(subcontainers); + let subcontainers = container + .subcontainers + .lock() + .await + .iter() + .map(format_subcontainer_pair) + .join("\n"); + return Err(Error::new( + eyre!("no matching subcontainers are running for {id}; some possible choices are:\n{subcontainers}"), + ErrorKind::NotFound, + )); + }; + + let passwd = root_dir + .join("media/startos/subcontainers") + .join(subcontainer_id.as_ref()) + .join("etc") + .join("passwd"); + + let root_command = get_passwd_root_command(passwd).await; + + let workdir = attach_workdir(&image_id, &root_dir).await?; + + if subcontainer_ids.len() > 1 { + let subcontainer_ids = subcontainer_ids + .into_iter() + .map(format_subcontainer_pair) + .join("\n"); + return Err(Error::new( + eyre!("multiple subcontainers found for {id}: \n{subcontainer_ids}"), + ErrorKind::InvalidRequest, + )); + } + + ( + service_ref.container_id()?, + subcontainer_id, + image_id, + workdir, + root_command, + ) + }; + + let guid = Guid::new(); + async fn handler( + ws: &mut WebSocket, + container_id: ContainerId, + subcontainer_id: Guid, + command: Vec, + tty: bool, + image_id: ImageId, + workdir: Option, + root_command: &RootCommand, + ) -> Result<(), Error> { + use axum::extract::ws::Message; + + let mut ws = ws.fuse(); + + let mut cmd = Command::new("lxc-attach"); + let root_path = Path::new("/media/startos/subcontainers").join(subcontainer_id.as_ref()); + cmd.kill_on_drop(true); + + cmd.arg(&*container_id) + .arg("--") + .arg("start-cli") + .arg("subcontainer") + .arg("exec") + .arg("--env") + .arg( + Path::new("/media/startos/images") + .join(image_id) + .with_extension("env"), + ); + + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + + if tty { + cmd.arg("--force-tty"); + } + + cmd.arg(&root_path).arg("--"); + + if command.is_empty() { + cmd.arg(&root_command.0); + } else { + cmd.args(&command); + } + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let pid = nix::unistd::Pid::from_raw(child.id().or_not_found("child pid")? as i32); + + let mut stdin = child.stdin.take().or_not_found("child stdin")?; + + let mut current_in = "stdin".to_owned(); + let mut current_out = "stdout"; + ws.send(Message::Text(current_out.into())) + .await + .with_kind(ErrorKind::Network)?; + let mut stdout = AsyncReadStream::new( + child.stdout.take().or_not_found("child stdout")?, + 4 * CAP_1_KiB, + ) + .fuse(); + let mut stderr = AsyncReadStream::new( + child.stderr.take().or_not_found("child stderr")?, + 4 * CAP_1_KiB, + ) + .fuse(); + + loop { + futures::select_biased! { + out = stdout.try_next() => { + if let Some(out) = out? { + if current_out != "stdout" { + ws.send(Message::Text("stdout".into())) + .await + .with_kind(ErrorKind::Network)?; + current_out = "stdout"; + } + ws.send(Message::Binary(out)) + .await + .with_kind(ErrorKind::Network)?; + } + } + err = stderr.try_next() => { + if let Some(err) = err? { + if current_out != "stderr" { + ws.send(Message::Text("stderr".into())) + .await + .with_kind(ErrorKind::Network)?; + current_out = "stderr"; + } + ws.send(Message::Binary(err)) + .await + .with_kind(ErrorKind::Network)?; + } + } + msg = ws.try_next() => { + if let Some(msg) = msg.with_kind(ErrorKind::Network)? { + match msg { + Message::Text(in_ty) => { + current_in = in_ty; + } + Message::Binary(data) => { + match &*current_in { + "stdin" => { + stdin.write_all(&data).await?; + } + "signal" => { + if data.len() != 4 { + return Err(Error::new( + eyre!("invalid byte length for signal: {}", data.len()), + ErrorKind::InvalidRequest + )); + } + let mut sig_buf = [0u8; 4]; + sig_buf.clone_from_slice(&data); + nix::sys::signal::kill( + pid, + Signal::try_from(i32::from_be_bytes(sig_buf)) + .with_kind(ErrorKind::InvalidRequest)? + ).with_kind(ErrorKind::Filesystem)?; + } + _ => (), + } + } + _ => () + } + } else { + return Ok(()) + } + } + } + if stdout.is_terminated() && stderr.is_terminated() { + break; + } + } + + let exit = child.wait().await?; + ws.send(Message::Text("exit".into())) + .await + .with_kind(ErrorKind::Network)?; + ws.send(Message::Binary(i32::to_be_bytes(exit.into_raw()).to_vec())) + .await + .with_kind(ErrorKind::Network)?; + + Ok(()) + } + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + move |mut ws| async move { + if let Err(e) = handler( + &mut ws, + container_id, + subcontainer_id, + command, + tty, + image_id, + workdir, + &root_command, + ) + .await + { + tracing::error!("Error in attach websocket: {e}"); + tracing::debug!("{e:?}"); + ws.close_result(Err::<&str, _>(e)).await.log_err(); + } else { + ws.normal_close("exit").await.log_err(); + } + }, + Duration::from_secs(30), + ), + ) + .await; + + Ok(guid) +} + +async fn attach_workdir(image_id: &ImageId, root_dir: &Path) -> Result, Error> { + let path_str = root_dir.join("media/startos/images/"); + + let mut subcontainer_json = + tokio::fs::File::open(path_str.join(image_id).with_extension("json")).await?; + let mut contents = vec![]; + subcontainer_json.read_to_end(&mut contents).await?; + let subcontainer_json: serde_json::Value = + serde_json::from_slice(&contents).with_kind(ErrorKind::Filesystem)?; + Ok(subcontainer_json["workdir"].as_str().map(|x| x.to_string())) +} + +async fn get_passwd_root_command(etc_passwd_path: PathBuf) -> RootCommand { + async { + let mut file = tokio::fs::File::open(etc_passwd_path).await?; + + let mut contents = vec![]; + file.read_to_end(&mut contents).await?; + + let contents = String::from_utf8_lossy(&contents); + + for line in contents.split('\n') { + let line_information = line.split(':').collect::>(); + if let (Some(&"root"), Some(shell)) = + (line_information.first(), line_information.last()) + { + return Ok(shell.to_string()); + } + } + Err(Error::new( + eyre!("Could not parse /etc/passwd for shell: {}", contents), + ErrorKind::Filesystem, + )) + } + .await + .map(RootCommand) + .unwrap_or_else(|e| { + tracing::error!("Could not get the /etc/passwd: {e}"); + tracing::debug!("{e:?}"); + RootCommand("/bin/sh".to_string()) + }) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct CliAttachParams { + pub id: PackageId, + #[arg(long)] + pub force_tty: bool, + #[arg(trailing_var_arg = true)] + pub command: Vec, + #[arg(long, short)] + subcontainer: Option, + #[arg(long, short)] + name: Option, + #[arg(long, short)] + image_id: Option, +} +pub async fn cli_attach( + HandlerArgs { + context, + parent_method, + method, + params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + use tokio_tungstenite::tungstenite::Message; + + let guid: Guid = from_value( + context + .call_remote::( + &parent_method.into_iter().chain(method).join("."), + json!({ + "id": params.id, + "command": params.command, + "tty": (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || params.force_tty, + "subcontainer": params.subcontainer, + "imageId": params.image_id, + "name": params.name, + }), + ) + .await?, + )?; + let mut ws = context.ws_continuation(guid).await?; + + let mut current_in = "stdin"; + let mut current_out = "stdout".to_owned(); + ws.send(Message::Text(current_in.into())) + .await + .with_kind(ErrorKind::Network)?; + let mut stdin = AsyncReadStream::new(tokio::io::stdin(), 4 * CAP_1_KiB).fuse(); + let mut stdout = tokio::io::stdout(); + let mut stderr = tokio::io::stderr(); + loop { + futures::select_biased! { + // signal = tokio:: => { + // let exit = exit?; + // if current_out != "exit" { + // ws.send(Message::Text("exit".into())) + // .await + // .with_kind(ErrorKind::Network)?; + // current_out = "exit"; + // } + // ws.send(Message::Binary( + // i32::to_be_bytes(exit.into_raw()).to_vec() + // )).await.with_kind(ErrorKind::Network)?; + // } + input = stdin.try_next() => { + if let Some(input) = input? { + if current_in != "stdin" { + ws.send(Message::Text("stdin".into())) + .await + .with_kind(ErrorKind::Network)?; + current_in = "stdin"; + } + ws.send(Message::Binary(input)) + .await + .with_kind(ErrorKind::Network)?; + } + } + msg = ws.try_next() => { + if let Some(msg) = msg.with_kind(ErrorKind::Network)? { + match msg { + Message::Text(out_ty) => { + current_out = out_ty; + } + Message::Binary(data) => { + match &*current_out { + "stdout" => { + stdout.write_all(&data).await?; + stdout.flush().await?; + } + "stderr" => { + stderr.write_all(&data).await?; + stderr.flush().await?; + } + "exit" => { + if data.len() != 4 { + return Err(Error::new( + eyre!("invalid byte length for exit code: {}", data.len()), + ErrorKind::InvalidRequest + )); + } + let mut exit_buf = [0u8; 4]; + exit_buf.clone_from_slice(&data); + let code = i32::from_be_bytes(exit_buf); + std::process::exit(code); + } + _ => (), + } + } + Message::Close(Some(close)) => { + if close.code != CloseCode::Normal { + return Err(Error::new( + color_eyre::eyre::Report::msg(close.reason), + ErrorKind::Network + )); + } + } + _ => () + } + } else { + return Ok(()) + } + } + } + } +} diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs new file mode 100644 index 000000000..910255e7d --- /dev/null +++ b/core/startos/src/service/persistent_container.rs @@ -0,0 +1,576 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Deref; +use std::path::Path; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +use futures::future::ready; +use futures::Future; +use helpers::NonDetachingJoinHandle; +use imbl::Vector; +use imbl_value::InternedString; +use models::{ImageId, ProcedureName, VolumeId}; +use rpc_toolkit::{Empty, Server, ShutdownHandle}; +use serde::de::DeserializeOwned; +use tokio::process::Command; +use tokio::sync::{oneshot, watch, Mutex, OnceCell}; +use tracing::instrument; + +use crate::context::RpcContext; +use crate::disk::mount::filesystem::bind::Bind; +use crate::disk::mount::filesystem::idmapped::IdMapped; +use crate::disk::mount::filesystem::loop_dev::LoopDev; +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::disk::mount::filesystem::{MountType, ReadOnly}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; +use crate::lxc::{LxcConfig, LxcContainer, HOST_RPC_SERVER_SOCKET}; +use crate::net::net_controller::NetService; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; +use crate::service::effects::context::EffectContext; +use crate::service::effects::handler; +use crate::service::rpc::{CallbackHandle, CallbackId, CallbackParams}; +use crate::service::start_stop::StartStop; +use crate::service::transition::{TransitionKind, TransitionState}; +use crate::service::{rpc, RunningStatus, Service}; +use crate::util::io::create_file; +use crate::util::rpc_client::UnixRpcClient; +use crate::util::Invoke; +use crate::volume::data_dir; +use crate::{ARCH, DATA_DIR, PACKAGE_DATA}; + +const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Debug)] +pub struct ServiceState { + // indicates whether the service container runtime has been initialized yet + pub(super) rt_initialized: bool, + // This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db, + pub(super) running_status: Option, + // This tracks references to callbacks registered by the running service: + pub(super) callbacks: BTreeSet>, + /// Setting this value causes the service actor to try to bring the service to the specified state. This is done in the background job created in ServiceActor::init + pub(super) desired_state: StartStop, + /// Override the current desired state for the service during a transition (this is protected by a guard that sets this value to null on drop) + pub(super) temp_desired_state: Option, + /// This represents a currently running task that affects the service's shown state, such as BackingUp or Restarting. + pub(super) transition_state: Option, +} + +#[derive(Debug)] +pub struct ServiceStateKinds { + pub transition_state: Option, + pub running_status: Option, + pub desired_state: StartStop, +} + +impl ServiceState { + pub fn new(desired_state: StartStop) -> Self { + Self { + rt_initialized: false, + running_status: Default::default(), + callbacks: Default::default(), + temp_desired_state: Default::default(), + transition_state: Default::default(), + desired_state, + } + } + pub fn kinds(&self) -> ServiceStateKinds { + ServiceStateKinds { + transition_state: self.transition_state.as_ref().map(|x| x.kind()), + desired_state: self.temp_desired_state.unwrap_or(self.desired_state), + running_status: self.running_status.clone(), + } + } +} + +/// Want to have a wrapper for uses like the inject where we are going to be finding the subcontainer and doing some filtering on it. +/// As well, the imageName is also used for things like env. +pub struct Subcontainer { + pub(super) name: InternedString, + pub(super) image_id: ImageId, + pub(super) overlay: OverlayGuard>, +} + +// @DRB On top of this we need to also have the procedures to have the effects and get the results back for them, maybe lock them to the running instance? +/// This contains the LXC container running the javascript init system +/// that can be used via a JSON RPC Client connected to a unix domain +/// socket served by the container +pub struct PersistentContainer { + pub(super) s9pk: S9pk, + pub(super) lxc_container: OnceCell, + pub(super) rpc_client: UnixRpcClient, + pub(super) rpc_server: watch::Sender, ShutdownHandle)>>, + // procedures: Mutex>, + js_mount: MountGuard, + volumes: BTreeMap, + assets: BTreeMap, + pub(super) images: BTreeMap>, + pub(super) subcontainers: Arc>>, + pub(super) state: Arc>, + pub(super) net_service: NetService, + destroyed: bool, +} + +impl PersistentContainer { + #[instrument(skip_all)] + pub async fn new(ctx: &RpcContext, s9pk: S9pk, start: StartStop) -> Result { + let lxc_container = ctx + .lxc_manager + .create( + Some( + &Path::new(PACKAGE_DATA) + .join("logs") + .join(&s9pk.as_manifest().id), + ), + LxcConfig::default(), + ) + .await?; + let rpc_client = lxc_container.connect_rpc(Some(RPC_CONNECT_TIMEOUT)).await?; + let js_mount = MountGuard::mount( + &LoopDev::from( + &**s9pk + .as_archive() + .contents() + .get_path("javascript.squashfs") + .and_then(|f| f.as_file()) + .or_not_found("javascript")?, + ), + lxc_container.rootfs_dir().join("usr/lib/startos/package"), + ReadOnly, + ) + .await?; + + let mut volumes = BTreeMap::new(); + for volume in &s9pk.as_manifest().volumes { + let mountpoint = lxc_container + .rootfs_dir() + .join("media/startos/volumes") + .join(volume); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + let mount = MountGuard::mount( + &IdMapped::new( + Bind::new(data_dir(DATA_DIR, &s9pk.as_manifest().id, volume)), + 0, + 100000, + 65536, + ), + mountpoint, + MountType::ReadWrite, + ) + .await?; + volumes.insert(volume.clone(), mount); + } + let mut assets = BTreeMap::new(); + for asset in &s9pk.as_manifest().assets { + let mountpoint = lxc_container + .rootfs_dir() + .join("media/startos/assets") + .join(asset); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + let s9pk_asset_path = Path::new("assets").join(asset).with_extension("squashfs"); + let sqfs = s9pk + .as_archive() + .contents() + .get_path(&s9pk_asset_path) + .and_then(|e| e.as_file()) + .or_not_found(s9pk_asset_path.display())?; + assets.insert( + asset.clone(), + MountGuard::mount( + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), + mountpoint, + MountType::ReadWrite, + ) + .await?, + ); + } + + let mut images = BTreeMap::new(); + let image_path = lxc_container.rootfs_dir().join("media/startos/images"); + tokio::fs::create_dir_all(&image_path).await?; + for (image, config) in &s9pk.as_manifest().images { + let mut arch = ARCH; + let mut sqfs_path = Path::new("images") + .join(arch) + .join(image) + .with_extension("squashfs"); + if !s9pk + .as_archive() + .contents() + .get_path(&sqfs_path) + .and_then(|e| e.as_file()) + .is_some() + { + arch = if let Some(arch) = config.emulate_missing_as.as_deref() { + arch + } else { + continue; + }; + sqfs_path = Path::new("images") + .join(arch) + .join(image) + .with_extension("squashfs"); + } + let sqfs = s9pk + .as_archive() + .contents() + .get_path(&sqfs_path) + .and_then(|e| e.as_file()) + .or_not_found(sqfs_path.display())?; + let mountpoint = image_path.join(image); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(ErrorKind::Filesystem) + .await?; + images.insert( + image.clone(), + Arc::new( + MountGuard::mount( + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), + &mountpoint, + ReadOnly, + ) + .await?, + ), + ); + let env_filename = Path::new(image.as_ref()).with_extension("env"); + if let Some(env) = s9pk + .as_archive() + .contents() + .get_path(Path::new("images").join(arch).join(&env_filename)) + .and_then(|e| e.as_file()) + { + env.copy(&mut create_file(image_path.join(&env_filename)).await?) + .await?; + } + let json_filename = Path::new(image.as_ref()).with_extension("json"); + if let Some(json) = s9pk + .as_archive() + .contents() + .get_path(Path::new("images").join(arch).join(&json_filename)) + .and_then(|e| e.as_file()) + { + json.copy(&mut create_file(image_path.join(&json_filename)).await?) + .await?; + } + } + let net_service = ctx + .net_controller + .create_service(s9pk.as_manifest().id.clone(), lxc_container.ip().await?) + .await?; + Ok(Self { + s9pk, + lxc_container: OnceCell::new_with(Some(lxc_container)), + rpc_client, + rpc_server: watch::channel(None).0, + // procedures: Default::default(), + js_mount, + volumes, + assets, + images, + subcontainers: Arc::new(Mutex::new(BTreeMap::new())), + state: Arc::new(watch::channel(ServiceState::new(start)).0), + net_service, + destroyed: false, + }) + } + + #[instrument(skip_all)] + pub async fn mount_backup( + &self, + backup_path: impl AsRef, + mount_type: MountType, + ) -> Result { + let backup_path = backup_path.as_ref(); + let mountpoint = self + .lxc_container + .get() + .ok_or_else(|| { + Error::new( + eyre!("PersistentContainer has been destroyed"), + ErrorKind::Incoherent, + ) + })? + .rootfs_dir() + .join("media/startos/backup"); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(mountpoint.as_os_str()) + .invoke(ErrorKind::Filesystem) + .await?; + tokio::fs::create_dir_all(backup_path).await?; + Command::new("chown") + .arg("100000:100000") + .arg(backup_path) + .invoke(ErrorKind::Filesystem) + .await?; + let bind = Bind::new(backup_path); + MountGuard::mount(&bind, &mountpoint, mount_type).await + } + + #[instrument(skip_all)] + pub async fn init(&self, seed: Weak) -> Result<(), Error> { + let socket_server_context = EffectContext::new(seed); + let server = Server::new(move || ready(Ok(socket_server_context.clone())), handler()); + let path = self + .lxc_container + .get() + .ok_or_else(|| { + Error::new( + eyre!("PersistentContainer has been destroyed"), + ErrorKind::Incoherent, + ) + })? + .rpc_dir() + .join(HOST_RPC_SERVER_SOCKET); + let (send, recv) = oneshot::channel(); + let handle = NonDetachingJoinHandle::from(tokio::spawn(async move { + let chown_status = async { + let res = server.run_unix(&path, |err| { + tracing::error!("error on unix socket {}: {err}", path.display()) + })?; + Command::new("chown") + .arg("100000:100000") + .arg(&path) + .invoke(ErrorKind::Filesystem) + .await?; + Ok::<_, Error>(res) + }; + let (shutdown, fut) = match chown_status.await { + Ok((shutdown, fut)) => (Ok(shutdown), Some(fut)), + Err(e) => (Err(e), None), + }; + if send.send(shutdown).is_err() { + panic!("failed to send shutdown handle"); + } + if let Some(fut) = fut { + fut.await; + } + })); + let shutdown = recv.await.map_err(|_| { + Error::new( + eyre!("unix socket server thread panicked"), + ErrorKind::Unknown, + ) + })??; + if self + .rpc_server + .send_replace(Some((handle, shutdown))) + .is_some() + { + return Err(Error::new( + eyre!("PersistentContainer already initialized"), + ErrorKind::InvalidRequest, + )); + } + + self.rpc_client.request(rpc::Init, Empty {}).await?; + + self.state.send_modify(|s| s.rt_initialized = true); + + Ok(()) + } + + #[instrument(skip_all)] + fn destroy( + &mut self, + error: bool, + ) -> Option> + 'static> { + if self.destroyed { + return None; + } + let rpc_client = self.rpc_client.clone(); + let rpc_server = self.rpc_server.send_replace(None); + let js_mount = self.js_mount.take(); + let volumes = std::mem::take(&mut self.volumes); + let assets = std::mem::take(&mut self.assets); + let images = std::mem::take(&mut self.images); + let subcontainers = self.subcontainers.clone(); + let lxc_container = self.lxc_container.take(); + self.destroyed = true; + Some(async move { + let mut errs = ErrorCollection::new(); + if error { + if let Some(lxc_container) = &lxc_container { + if let Some(logs) = errs.handle( + crate::logs::fetch_logs( + crate::logs::LogSource::Container(lxc_container.guid.deref().clone()), + Some(50), + None, + None, + false, + ) + .await, + ) { + for log in logs.entries.iter() { + eprintln!("{log}"); + } + } + } + } + if let Some((hdl, shutdown)) = rpc_server { + errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); + shutdown.shutdown(); + errs.handle(hdl.await.with_kind(ErrorKind::Cancelled)); + } + for (_, volume) in volumes { + errs.handle(volume.unmount(true).await); + } + for (_, assets) in assets { + errs.handle(assets.unmount(true).await); + } + for (_, overlay) in std::mem::take(&mut *subcontainers.lock().await) { + errs.handle(overlay.overlay.unmount(true).await); + } + for (_, images) in images { + errs.handle(images.unmount().await); + } + errs.handle(js_mount.unmount(true).await); + if let Some(lxc_container) = lxc_container { + errs.handle(lxc_container.exit().await); + } + errs.into_result() + }) + } + + #[instrument(skip_all)] + pub async fn exit(mut self) -> Result<(), Error> { + if let Some(destroy) = self.destroy(false) { + destroy.await?; + } + tracing::info!("Service for {} exited", self.s9pk.as_manifest().id); + + Ok(()) + } + + #[instrument(skip_all)] + pub async fn start(&self) -> Result<(), Error> { + self.rpc_client.request(rpc::Start, Empty {}).await?; + Ok(()) + } + + #[instrument(skip_all)] + pub async fn stop(&self) -> Result<(), Error> { + self.rpc_client.request(rpc::Stop, Empty {}).await?; + Ok(()) + } + + #[instrument(skip_all)] + pub async fn execute( + &self, + id: Guid, + name: ProcedureName, + input: Value, + timeout: Option, + ) -> Result + where + O: DeserializeOwned, + { + self._execute(id, name, input, timeout) + .await + .and_then(from_value) + } + + #[instrument(skip_all)] + pub async fn sanboxed( + &self, + id: Guid, + name: ProcedureName, + input: Value, + timeout: Option, + ) -> Result + where + O: DeserializeOwned, + { + self._sandboxed(id, name, input, timeout) + .await + .and_then(from_value) + } + + #[instrument(skip_all)] + pub async fn callback(&self, handle: CallbackHandle, args: Vector) -> Result<(), Error> { + let mut params = None; + self.state.send_if_modified(|s| { + params = handle.params(&mut s.callbacks, args); + params.is_some() + }); + if let Some(params) = params { + self._callback(params).await?; + } + Ok(()) + } + + #[instrument(skip_all)] + async fn _execute( + &self, + id: Guid, + name: ProcedureName, + input: Value, + timeout: Option, + ) -> Result { + let fut = self.rpc_client.request( + rpc::Execute, + rpc::ExecuteParams::new(id, name, input, timeout), + ); + + Ok(if let Some(timeout) = timeout { + tokio::time::timeout(timeout, fut) + .await + .with_kind(ErrorKind::Timeout)?? + } else { + fut.await? + }) + } + + #[instrument(skip_all)] + async fn _sandboxed( + &self, + id: Guid, + name: ProcedureName, + input: Value, + timeout: Option, + ) -> Result { + let fut = self.rpc_client.request( + rpc::Sandbox, + rpc::ExecuteParams::new(id, name, input, timeout), + ); + + Ok(if let Some(timeout) = timeout { + tokio::time::timeout(timeout, fut) + .await + .with_kind(ErrorKind::Timeout)?? + } else { + fut.await? + }) + } + + #[instrument(skip_all)] + async fn _callback(&self, params: CallbackParams) -> Result<(), Error> { + self.rpc_client.notify(rpc::Callback, params).await?; + Ok(()) + } +} + +impl Drop for PersistentContainer { + fn drop(&mut self) { + if let Some(destroy) = self.destroy(true) { + tokio::spawn(async move { destroy.await.log_err() }); + } + } +} diff --git a/core/startos/src/service/rpc.rs b/core/startos/src/service/rpc.rs new file mode 100644 index 000000000..61eb5d592 --- /dev/null +++ b/core/startos/src/service/rpc.rs @@ -0,0 +1,237 @@ +use std::collections::BTreeSet; +use std::str::FromStr; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +use clap::builder::ValueParserFactory; +use imbl::Vector; +use imbl_value::Value; +use models::{FromStrParser, ProcedureName}; +use rpc_toolkit::yajrc::RpcMethod; +use rpc_toolkit::Empty; +use ts_rs::TS; + +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::persistent_container::PersistentContainer; +use crate::util::Never; + +#[derive(Clone)] +pub struct Init; +impl RpcMethod for Init { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "init" + } +} +impl serde::Serialize for Init { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone)] +pub struct Start; +impl RpcMethod for Start { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "start" + } +} +impl serde::Serialize for Start { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone)] +pub struct Stop; +impl RpcMethod for Stop { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "stop" + } +} +impl serde::Serialize for Stop { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone)] +pub struct Exit; +impl RpcMethod for Exit { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "exit" + } +} +impl serde::Serialize for Exit { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone, serde::Deserialize, serde::Serialize, TS)] +pub struct ExecuteParams { + id: Guid, + procedure: String, + #[ts(type = "any")] + input: Value, + timeout: Option, +} +impl ExecuteParams { + pub fn new( + id: Guid, + procedure: ProcedureName, + input: Value, + timeout: Option, + ) -> Self { + Self { + id, + procedure: procedure.js_function_name(), + input, + timeout: timeout.map(|d| d.as_millis()), + } + } +} + +#[derive(Clone)] +pub struct Execute; +impl RpcMethod for Execute { + type Params = ExecuteParams; + type Response = Value; + fn as_str<'a>(&'a self) -> &'a str { + "execute" + } +} +impl serde::Serialize for Execute { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone)] +pub struct Sandbox; +impl RpcMethod for Sandbox { + type Params = ExecuteParams; + type Response = Value; + fn as_str<'a>(&'a self) -> &'a str { + "sandbox" + } +} +impl serde::Serialize for Sandbox { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive( + Clone, Copy, Debug, serde::Deserialize, serde::Serialize, TS, PartialEq, Eq, PartialOrd, Ord, +)] +#[ts(type = "number")] +pub struct CallbackId(u64); +impl CallbackId { + pub fn register(self, container: &PersistentContainer) -> CallbackHandle { + crate::dbg!(eyre!( + "callback {} registered for {}", + self.0, + container.s9pk.as_manifest().id + )); + let this = Arc::new(self); + let res = Arc::downgrade(&this); + container + .state + .send_if_modified(|s| s.callbacks.insert(this)); + CallbackHandle(res) + } +} +impl FromStr for CallbackId { + type Err = Error; + fn from_str(s: &str) -> Result { + u64::from_str(s).map_err(Error::from).map(Self) + } +} +impl ValueParserFactory for CallbackId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +pub struct CallbackHandle(Weak); +impl CallbackHandle { + pub fn is_active(&self) -> bool { + self.0.strong_count() > 0 + } + pub fn params( + self, + registered: &mut BTreeSet>, + args: Vector, + ) -> Option { + if let Some(id) = self.0.upgrade() { + if let Some(strong) = registered.get(&id) { + if Arc::ptr_eq(strong, &id) { + registered.remove(&id); + return Some(CallbackParams::new(&*id, args)); + } + } + } + None + } + pub fn take(&mut self) -> Self { + Self(std::mem::take(&mut self.0)) + } +} + +#[derive(Clone, serde::Deserialize, serde::Serialize, TS)] +pub struct CallbackParams { + id: u64, + #[ts(type = "any[]")] + args: Vector, +} +impl CallbackParams { + fn new(id: &CallbackId, args: Vector) -> Self { + Self { id: id.0, args } + } +} + +#[derive(Clone)] +pub struct Callback; +impl RpcMethod for Callback { + type Params = CallbackParams; + type Response = Never; + fn as_str<'a>(&'a self) -> &'a str { + "callback" + } +} +impl serde::Serialize for Callback { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/core/startos/src/service/service_actor.rs b/core/startos/src/service/service_actor.rs new file mode 100644 index 000000000..a56c92288 --- /dev/null +++ b/core/startos/src/service/service_actor.rs @@ -0,0 +1,149 @@ +use std::sync::Arc; +use std::time::Duration; + +use imbl::vector; + +use super::start_stop::StartStop; +use super::ServiceActorSeed; +use crate::prelude::*; +use crate::service::persistent_container::ServiceStateKinds; +use crate::service::transition::TransitionKind; +use crate::service::SYNC_RETRY_COOLDOWN_SECONDS; +use crate::status::MainStatus; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::Actor; + +#[derive(Clone)] +pub(super) struct ServiceActor(pub(super) Arc); + +enum ServiceActorLoopNext { + Wait, + DontWait, +} + +impl Actor for ServiceActor { + fn init(&mut self, jobs: &BackgroundJobQueue) { + let seed = self.0.clone(); + let mut current = seed.persistent_container.state.subscribe(); + jobs.add_job(async move { + let _ = current.wait_for(|s| s.rt_initialized).await; + + loop { + match service_actor_loop(¤t, &seed).await { + ServiceActorLoopNext::Wait => tokio::select! { + _ = current.changed() => (), + }, + ServiceActorLoopNext::DontWait => (), + } + } + }); + } +} + +async fn service_actor_loop( + current: &tokio::sync::watch::Receiver, + seed: &Arc, +) -> ServiceActorLoopNext { + let id = &seed.id; + let kinds = current.borrow().kinds(); + if let Err(e) = async { + let major_changes_state = seed + .ctx + .db + .mutate(|d| { + if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let previous = i.as_status().de()?; + let main_status = match &kinds { + ServiceStateKinds { + transition_state: Some(TransitionKind::Restarting), + .. + } => MainStatus::Restarting, + ServiceStateKinds { + transition_state: Some(TransitionKind::Restoring), + .. + } => MainStatus::Restoring, + ServiceStateKinds { + transition_state: Some(TransitionKind::BackingUp), + .. + } => previous.backing_up(), + ServiceStateKinds { + running_status: Some(status), + desired_state: StartStop::Start, + .. + } => MainStatus::Running { + started: status.started, + health: previous.health().cloned().unwrap_or_default(), + }, + ServiceStateKinds { + running_status: None, + desired_state: StartStop::Start, + .. + } => MainStatus::Starting { + health: previous.health().cloned().unwrap_or_default(), + }, + ServiceStateKinds { + running_status: Some(_), + desired_state: StartStop::Stop, + .. + } => MainStatus::Stopping, + ServiceStateKinds { + running_status: None, + desired_state: StartStop::Stop, + .. + } => MainStatus::Stopped, + }; + i.as_status_mut().ser(&main_status)?; + return Ok(previous + .major_changes(&main_status) + .then_some((previous, main_status))); + } + Ok(None) + }) + .await?; + if let Some((previous, new_state)) = major_changes_state { + if let Some(callbacks) = seed.ctx.callbacks.get_status(id) { + callbacks + .call(vector![to_value(&previous)?, to_value(&new_state)?]) + .await?; + } + } + seed.synchronized.notify_waiters(); + + match kinds { + ServiceStateKinds { + running_status: None, + desired_state: StartStop::Start, + .. + } => { + seed.persistent_container.start().await?; + } + ServiceStateKinds { + running_status: Some(_), + desired_state: StartStop::Stop, + .. + } => { + seed.persistent_container.stop().await?; + seed.persistent_container + .state + .send_if_modified(|s| s.running_status.take().is_some()); + } + _ => (), + }; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("error synchronizing state of service: {e}"); + tracing::debug!("{e:?}"); + + seed.synchronized.notify_waiters(); + + tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS); + tokio::time::sleep(Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS)).await; + return ServiceActorLoopNext::DontWait; + } + seed.synchronized.notify_waiters(); + + ServiceActorLoopNext::Wait +} diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs new file mode 100644 index 000000000..de450706e --- /dev/null +++ b/core/startos/src/service/service_map.rs @@ -0,0 +1,426 @@ +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +use color_eyre::eyre::eyre; +use futures::future::BoxFuture; +use futures::stream::FuturesUnordered; +use futures::{Future, FutureExt, StreamExt}; +use helpers::NonDetachingJoinHandle; +use imbl::OrdMap; +use imbl_value::InternedString; +use models::ErrorData; +use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; +use tracing::instrument; + +use crate::context::RpcContext; +use crate::db::model::package::{ + InstallingInfo, InstallingState, PackageDataEntry, PackageState, UpdatingState, +}; +use crate::disk::mount::guard::GenericMountGuard; +use crate::install::PKG_ARCHIVE_DIR; +use crate::notifications::{notify, NotificationLevel}; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter}; +use crate::s9pk::manifest::PackageId; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; +use crate::service::start_stop::StartStop; +use crate::service::{LoadDisposition, Service, ServiceRef}; +use crate::status::MainStatus; +use crate::util::serde::Pem; +use crate::DATA_DIR; + +pub type DownloadInstallFuture = BoxFuture<'static, Result>; +pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; + +pub struct InstallProgressHandles { + pub finalization_progress: PhaseProgressTrackerHandle, + pub progress: FullProgressTracker, +} + +/// This is the structure to contain all the services +#[derive(Default)] +pub struct ServiceMap(Mutex>>>>); +impl ServiceMap { + async fn entry(&self, id: &PackageId) -> Arc>> { + let mut lock = self.0.lock().await; + lock.entry(id.clone()) + .or_insert_with(|| Arc::new(RwLock::new(None))) + .clone() + } + + #[instrument(skip_all)] + pub async fn get(&self, id: &PackageId) -> OwnedRwLockReadGuard> { + self.entry(id).await.read_owned().await + } + + #[instrument(skip_all)] + pub async fn get_mut(&self, id: &PackageId) -> OwnedRwLockWriteGuard> { + self.entry(id).await.write_owned().await + } + + #[instrument(skip_all)] + pub async fn init( + &self, + ctx: &RpcContext, + mut progress: PhaseProgressTrackerHandle, + ) -> Result<(), Error> { + progress.start(); + let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; + progress.set_total(ids.len() as u64); + let mut jobs = FuturesUnordered::new(); + for id in &ids { + jobs.push(self.load(ctx, id, LoadDisposition::Retry)); + } + while let Some(res) = jobs.next().await { + if let Err(e) = res { + tracing::error!("Error loading installed package as service: {e}"); + tracing::debug!("{e:?}"); + } + progress += 1; + } + progress.complete(); + Ok(()) + } + + #[instrument(skip_all)] + pub async fn load( + &self, + ctx: &RpcContext, + id: &PackageId, + disposition: LoadDisposition, + ) -> Result<(), Error> { + let mut shutdown_err = Ok(()); + let mut service = self.get_mut(id).await; + if let Some(service) = service.take() { + shutdown_err = service.shutdown().await; + } + match Service::load(ctx, id, disposition).await { + Ok(s) => *service = s.into(), + Err(e) => { + let e = ErrorData::from(e); + ctx.db + .mutate(|db| { + if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) { + pde.as_status_mut().map_mutate(|s| { + Ok(MainStatus::Error { + on_rebuild: if s.running() { + StartStop::Start + } else { + StartStop::Stop + }, + message: e.details, + debug: Some(e.debug), + }) + })?; + } + Ok(()) + }) + .await?; + } + } + shutdown_err?; + Ok(()) + } + + #[instrument(skip_all)] + pub async fn install( + &self, + ctx: RpcContext, + s9pk: F, + recovery_source: Option, + progress: Option, + ) -> Result + where + F: FnOnce() -> Fut, + Fut: Future, Error>>, + S: FileSource + Clone, + { + let mut s9pk = s9pk().await?; + s9pk.validate_and_filter(ctx.s9pk_arch)?; + let manifest = s9pk.as_manifest().clone(); + let id = manifest.id.clone(); + let icon = s9pk.icon_data_url().await?; + let developer_key = s9pk.as_archive().signer(); + let mut service = self.get_mut(&id).await; + + let op_name = if recovery_source.is_none() { + if service.is_none() { + "Install" + } else { + "Update" + } + } else { + "Restore" + }; + + let size = s9pk.size(); + let progress = progress.unwrap_or_else(|| FullProgressTracker::new()); + let download_progress_contribution = size.unwrap_or(60); + let mut download_progress = progress.add_phase( + InternedString::intern("Download"), + Some(download_progress_contribution), + ); + if let Some(size) = size { + download_progress.set_total(size); + } + let mut finalization_progress = progress.add_phase( + InternedString::intern(op_name), + Some(download_progress_contribution / 2), + ); + let restoring = recovery_source.is_some(); + + let mut reload_guard = ServiceRefReloadGuard::new(ctx.clone(), id.clone(), op_name); + + reload_guard + .handle(ctx.db.mutate({ + let manifest = manifest.clone(); + let id = id.clone(); + let install_progress = progress.snapshot(); + move |db| { + if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let prev = pde.as_state_info().expect_installed()?.de()?; + pde.as_state_info_mut() + .ser(&PackageState::Updating(UpdatingState { + manifest: prev.manifest, + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }))?; + } else { + let installing = InstallingState { + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }; + db.as_public_mut().as_package_data_mut().insert( + &id, + &PackageDataEntry { + state_info: if restoring { + PackageState::Restoring(installing) + } else { + PackageState::Installing(installing) + }, + data_version: None, + status: MainStatus::Stopped, + registry: None, + developer_key: Pem::new(developer_key), + icon, + last_backup: None, + current_dependencies: Default::default(), + actions: Default::default(), + requested_actions: Default::default(), + service_interfaces: Default::default(), + hosts: Default::default(), + store_exposed_dependents: Default::default(), + }, + )?; + }; + Ok(()) + } + })) + .await?; + + Ok(async move { + let (installed_path, sync_progress_task) = reload_guard + .handle(async { + let download_path = Path::new(DATA_DIR) + .join(PKG_ARCHIVE_DIR) + .join("downloading") + .join(&id) + .with_extension("s9pk"); + + let deref_id = id.clone(); + let sync_progress_task = + NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db( + ctx.db.clone(), + move |v| { + v.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&deref_id) + .and_then(|e| e.as_state_info_mut().as_installing_info_mut()) + .map(|i| i.as_progress_mut()) + }, + Some(Duration::from_millis(100)), + ))); + + download_progress.start(); + let mut progress_writer = ProgressTrackerWriter::new( + crate::util::io::create_file(&download_path).await?, + download_progress, + ); + s9pk.serialize(&mut progress_writer, true).await?; + let (file, mut download_progress) = progress_writer.into_inner(); + file.sync_all().await?; + download_progress.complete(); + + let installed_path = Path::new(DATA_DIR) + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(&id) + .with_extension("s9pk"); + + crate::util::io::rename(&download_path, &installed_path).await?; + + Ok::<_, Error>((installed_path, sync_progress_task)) + }) + .await?; + Ok(reload_guard + .handle_last(async move { + finalization_progress.start(); + let s9pk = S9pk::open(&installed_path, Some(&id)).await?; + let prev = if let Some(service) = service.take() { + ensure_code!( + recovery_source.is_none(), + ErrorKind::InvalidRequest, + "cannot restore over existing package" + ); + let version = service + .seed + .persistent_container + .s9pk + .as_manifest() + .version + .clone(); + service + .uninstall(Some(s9pk.as_manifest().version.clone())) + .await?; + progress.complete(); + Some(version) + } else { + None + }; + *service = Some( + Service::install( + ctx, + s9pk, + prev, + recovery_source, + Some(InstallProgressHandles { + finalization_progress, + progress, + }), + ) + .await? + .into(), + ); + drop(service); + + sync_progress_task.await.map_err(|_| { + Error::new(eyre!("progress sync task panicked"), ErrorKind::Unknown) + })??; + Ok(()) + }) + .boxed()) + } + .boxed()) + } + + /// This is ran during the cleanup, so when we are uninstalling the service + #[instrument(skip_all)] + pub async fn uninstall(&self, ctx: &RpcContext, id: &PackageId) -> Result<(), Error> { + let mut guard = self.get_mut(id).await; + if let Some(service) = guard.take() { + ServiceRefReloadGuard::new(ctx.clone(), id.clone(), "Uninstall") + .handle_last(async move { + let res = service.uninstall(None).await; + drop(guard); + res + }) + .await?; + } + Ok(()) + } + + pub async fn shutdown_all(&self) -> Result<(), Error> { + let lock = self.0.lock().await; + let mut futs = Vec::with_capacity(lock.len()); + for service in lock.values().cloned() { + futs.push(async move { + if let Some(service) = service.write_owned().await.take() { + service.shutdown().await? + } + Ok::<_, Error>(()) + }); + } + drop(lock); + let mut errors = ErrorCollection::new(); + for res in futures::future::join_all(futs).await { + errors.handle(res); + } + errors.into_result() + } +} + +pub struct ServiceRefReloadGuard(Option); +impl Drop for ServiceRefReloadGuard { + fn drop(&mut self) { + if let Some(info) = self.0.take() { + tokio::spawn(info.reload(None)); + } + } +} +impl ServiceRefReloadGuard { + pub fn new(ctx: RpcContext, id: PackageId, operation: &'static str) -> Self { + Self(Some(ServiceRefReloadInfo { ctx, id, operation })) + } + + pub async fn handle( + &mut self, + operation: impl Future>, + ) -> Result { + let mut errors = ErrorCollection::new(); + match operation.await { + Ok(a) => Ok(a), + Err(e) => { + if let Some(info) = self.0.take() { + errors.handle(info.reload(Some(e.clone_output())).await); + } + errors.handle::<(), _>(Err(e)); + errors.into_result().map(|_| unreachable!()) // TODO: there's gotta be a more elegant way? + } + } + } + pub async fn handle_last( + mut self, + operation: impl Future>, + ) -> Result { + let res = self.handle(operation).await; + self.0.take(); + res + } +} + +struct ServiceRefReloadInfo { + ctx: RpcContext, + id: PackageId, + operation: &'static str, +} +impl ServiceRefReloadInfo { + async fn reload(self, error: Option) -> Result<(), Error> { + self.ctx + .services + .load(&self.ctx, &self.id, LoadDisposition::Undo) + .await?; + if let Some(error) = error { + let error_string = error.to_string(); + self.ctx + .db + .mutate(|db| { + notify( + db, + Some(self.id.clone()), + NotificationLevel::Error, + format!("{} Failed", self.operation), + error_string, + (), + ) + }) + .await?; + } + Ok(()) + } +} diff --git a/core/startos/src/service/start_stop.rs b/core/startos/src/service/start_stop.rs new file mode 100644 index 000000000..64d4022d6 --- /dev/null +++ b/core/startos/src/service/start_stop.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::status::MainStatus; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub enum StartStop { + Start, + Stop, +} + +impl StartStop { + pub(crate) fn is_start(&self) -> bool { + matches!(self, StartStop::Start) + } +} +// impl From for StartStop { +// fn from(value: MainStatus) -> Self { +// match value { +// MainStatus::Stopped => StartStop::Stop, +// MainStatus::Restoring => StartStop::Stop, +// MainStatus::Restarting => StartStop::Start, +// MainStatus::Stopping { .. } => StartStop::Stop, +// MainStatus::Starting => StartStop::Start, +// MainStatus::Running { +// started: _, +// health: _, +// } => StartStop::Start, +// MainStatus::BackingUp { on_complete } => on_complete, +// } +// } +// } diff --git a/core/startos/src/service/transition/backup.rs b/core/startos/src/service/transition/backup.rs new file mode 100644 index 000000000..6205cdd61 --- /dev/null +++ b/core/startos/src/service/transition/backup.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use futures::future::BoxFuture; +use futures::FutureExt; +use models::ProcedureName; + +use super::TempDesiredRestore; +use crate::disk::mount::filesystem::ReadWrite; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::action::GetActionInput; +use crate::service::transition::{TransitionKind, TransitionState}; +use crate::service::ServiceActor; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; +use crate::util::future::RemoteCancellable; +use crate::util::serde::NoOutput; + +pub(in crate::service) struct Backup { + pub path: PathBuf, +} +impl Handler for ServiceActor { + type Response = Result>, Error>; + fn conflicts_with(_: &Backup) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } + async fn handle( + &mut self, + id: Guid, + backup: Backup, + jobs: &BackgroundJobQueue, + ) -> Self::Response { + // So Need a handle to just a single field in the state + let temp: TempDesiredRestore = TempDesiredRestore::new(&self.0.persistent_container.state); + let mut current = self.0.persistent_container.state.subscribe(); + let path = backup.path.clone(); + let seed = self.0.clone(); + + let transition = RemoteCancellable::new(async move { + temp.stop(); + current + .wait_for(|s| s.running_status.is_none()) + .await + .with_kind(ErrorKind::Unknown)?; + + let backup_guard = seed + .persistent_container + .mount_backup(path, ReadWrite) + .await?; + seed.persistent_container + .execute::(id, ProcedureName::CreateBackup, Value::Null, None) + .await?; + backup_guard.unmount(true).await?; + + if temp.restore().is_start() { + current + .wait_for(|s| s.running_status.is_some()) + .await + .with_kind(ErrorKind::Unknown)?; + } + drop(temp); + Ok::<_, Arc>(()) + }); + let cancel_handle = transition.cancellation_handle(); + let transition = transition.shared(); + let job_transition = transition.clone(); + jobs.add_job(job_transition.map(|_| ())); + + let mut old = None; + self.0.persistent_container.state.send_modify(|s| { + old = std::mem::replace( + &mut s.transition_state, + Some(TransitionState { + kind: TransitionKind::BackingUp, + cancel_handle, + }), + ) + }); + if let Some(t) = old { + t.abort().await; + } + Ok(transition + .map(|r| { + r.ok_or_else(|| Error::new(eyre!("Backup canceled"), ErrorKind::Cancelled))? + .map_err(|e| e.clone_output()) + }) + .boxed()) + } +} diff --git a/core/startos/src/service/transition/mod.rs b/core/startos/src/service/transition/mod.rs new file mode 100644 index 000000000..a6a41073b --- /dev/null +++ b/core/startos/src/service/transition/mod.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use futures::{Future, FutureExt}; +use tokio::sync::watch; + +use super::persistent_container::ServiceState; +use crate::service::start_stop::StartStop; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::future::{CancellationHandle, RemoteCancellable}; + +pub mod backup; +pub mod restart; +pub mod restore; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum TransitionKind { + BackingUp, + Restarting, + Restoring, +} + +/// Used only in the manager/mod and is used to keep track of the state of the manager during the +/// transitional states +pub struct TransitionState { + cancel_handle: CancellationHandle, + kind: TransitionKind, +} +impl ::std::fmt::Debug for TransitionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransitionState") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +impl TransitionState { + pub fn kind(&self) -> TransitionKind { + self.kind + } + pub async fn abort(mut self) { + self.cancel_handle.cancel_and_wait().await + } + fn new( + task: impl Future + Send + 'static, + kind: TransitionKind, + jobs: &BackgroundJobQueue, + ) -> Self { + let task = RemoteCancellable::new(task); + let cancel_handle = task.cancellation_handle(); + jobs.add_job(task.map(|_| ())); + Self { + cancel_handle, + kind, + } + } +} +impl Drop for TransitionState { + fn drop(&mut self) { + self.cancel_handle.cancel(); + } +} + +#[derive(Debug, Clone)] +pub struct TempDesiredRestore(pub(super) Arc>, StartStop); +impl TempDesiredRestore { + pub fn new(state: &Arc>) -> Self { + Self(state.clone(), state.borrow().desired_state) + } + pub fn stop(&self) { + self.0 + .send_modify(|s| s.temp_desired_state = Some(StartStop::Stop)); + } + pub fn restore(&self) -> StartStop { + let restore_state = self.1; + self.0 + .send_modify(|s| s.temp_desired_state = Some(restore_state)); + restore_state + } +} +impl Drop for TempDesiredRestore { + fn drop(&mut self) { + self.0.send_modify(|s| { + s.temp_desired_state.take(); + s.transition_state.take(); + }); + } +} +// impl Deref for TempDesiredState { +// type Target = watch::Sender>; +// fn deref(&self) -> &Self::Target { +// &*self.0 +// } +// } diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs new file mode 100644 index 000000000..27bef0b91 --- /dev/null +++ b/core/startos/src/service/transition/restart.rs @@ -0,0 +1,78 @@ +use futures::FutureExt; + +use super::TempDesiredRestore; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::action::GetActionInput; +use crate::service::transition::{TransitionKind, TransitionState}; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; +use crate::util::future::RemoteCancellable; + +pub(super) struct Restart; +impl Handler for ServiceActor { + type Response = (); + fn conflicts_with(_: &Restart) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } + async fn handle(&mut self, _: Guid, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { + // So Need a handle to just a single field in the state + let temp = TempDesiredRestore::new(&self.0.persistent_container.state); + let mut current = self.0.persistent_container.state.subscribe(); + let state = self.0.persistent_container.state.clone(); + let transition = RemoteCancellable::new( + async move { + temp.stop(); + current + .wait_for(|s| s.running_status.is_none()) + .await + .with_kind(ErrorKind::Unknown)?; + if temp.restore().is_start() { + current + .wait_for(|s| s.running_status.is_some()) + .await + .with_kind(ErrorKind::Unknown)?; + } + drop(temp); + state.send_modify(|s| { + s.transition_state.take(); + }); + Ok::<_, Error>(()) + } + .map(|x| { + if let Err(err) = x { + tracing::debug!("{:?}", err); + tracing::warn!("{}", err); + } + }), + ); + let cancel_handle = transition.cancellation_handle(); + let transition = transition.shared(); + let job_transition = transition.clone(); + jobs.add_job(job_transition.map(|_| ())); + + let mut old = None; + self.0.persistent_container.state.send_modify(|s| { + old = std::mem::replace( + &mut s.transition_state, + Some(TransitionState { + kind: TransitionKind::Restarting, + cancel_handle, + }), + ) + }); + if let Some(t) = old { + t.abort().await; + } + if transition.await.is_none() { + tracing::warn!("Service {} has been cancelled", &self.0.id); + } + } +} +impl Service { + #[instrument(skip_all)] + pub async fn restart(&self, id: Guid) -> Result<(), Error> { + self.actor.send(id, Restart).await + } +} diff --git a/core/startos/src/service/transition/restore.rs b/core/startos/src/service/transition/restore.rs new file mode 100644 index 000000000..7061b0c1e --- /dev/null +++ b/core/startos/src/service/transition/restore.rs @@ -0,0 +1,81 @@ +use std::path::PathBuf; + +use futures::FutureExt; +use models::ProcedureName; + +use crate::disk::mount::filesystem::ReadOnly; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::service::transition::{TransitionKind, TransitionState}; +use crate::service::ServiceActor; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; +use crate::util::future::RemoteCancellable; +use crate::util::serde::NoOutput; + +pub(in crate::service) struct Restore { + pub path: PathBuf, +} +impl Handler for ServiceActor { + type Response = Result<(), Error>; + fn conflicts_with(_: &Restore) -> ConflictBuilder { + ConflictBuilder::everything() + } + async fn handle( + &mut self, + id: Guid, + restore: Restore, + jobs: &BackgroundJobQueue, + ) -> Self::Response { + // So Need a handle to just a single field in the state + let path = restore.path.clone(); + let seed = self.0.clone(); + + let state = self.0.persistent_container.state.clone(); + let transition = RemoteCancellable::new( + async move { + let backup_guard = seed + .persistent_container + .mount_backup(path, ReadOnly) + .await?; + seed.persistent_container + .execute::(id, ProcedureName::RestoreBackup, Value::Null, None) + .await?; + backup_guard.unmount(true).await?; + + state.send_modify(|s| { + s.transition_state.take(); + }); + Ok::<_, Error>(()) + } + .map(|x| { + if let Err(err) = x { + tracing::debug!("{:?}", err); + tracing::warn!("{}", err); + } + }), + ); + let cancel_handle = transition.cancellation_handle(); + let transition = transition.shared(); + let job_transition = transition.clone(); + jobs.add_job(job_transition.map(|_| ())); + + let mut old = None; + self.0.persistent_container.state.send_modify(|s| { + old = std::mem::replace( + &mut s.transition_state, + Some(TransitionState { + kind: TransitionKind::Restoring, + cancel_handle, + }), + ) + }); + if let Some(t) = old { + t.abort().await; + } + match transition.await { + None => Err(Error::new(eyre!("Restoring canceled"), ErrorKind::Unknown)), + Some(x) => Ok(x), + } + } +} diff --git a/core/startos/src/service/util.rs b/core/startos/src/service/util.rs new file mode 100644 index 000000000..3c53c2366 --- /dev/null +++ b/core/startos/src/service/util.rs @@ -0,0 +1,14 @@ +use futures::Future; +use tokio::sync::Notify; + +use crate::prelude::*; + +pub async fn cancellable( + cancel_transition: &Notify, + transition: impl Future, +) -> Result { + tokio::select! { + a = transition => Ok(a), + _ = cancel_transition.notified() => Err(Error::new(eyre!("transition was cancelled"), ErrorKind::Cancelled)), + } +} diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 64c324095..186fbb0c4 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -1,110 +1,133 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use color_eyre::eyre::eyre; +use const_format::formatcp; use josekit::jwk::Jwk; -use openssl::x509::X509; -use rpc_toolkit::command; +use patch_db::json_ptr::ROOT; use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use sqlx::Connection; -use tokio::fs::File; use tokio::io::AsyncWriteExt; +use tokio::process::Command; use tokio::try_join; -use torut::onion::OnionAddressV3; use tracing::instrument; +use ts_rs::TS; use crate::account::AccountInfo; use crate::backup::restore::recover_full_embassy; use crate::backup::target::BackupTargetFS; -use crate::context::rpc::RpcContextConfig; +use crate::context::rpc::InitRpcContextPhases; use crate::context::setup::SetupResult; -use crate::context::SetupContext; +use crate::context::{RpcContext, SetupContext}; +use crate::db::model::Database; use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::TmpMountGuard; -use crate::disk::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo}; +use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; +use crate::disk::util::{pvscan, recovery_info, DiskInfo, StartOsRecoveryInfo}; use crate::disk::REPAIR_DISK_PATH; -use crate::hostname::Hostname; -use crate::init::{init, InitResult}; -use crate::middleware::encrypt::EncryptedWire; +use crate::init::{init, InitPhases, InitResult}; +use crate::net::net_controller::NetController; use crate::net::ssl::root_ca_start_time; use crate::prelude::*; -use crate::util::io::{dir_copy, dir_size, Counter}; -use crate::{Error, ErrorKind, ResultExt}; - -#[command(subcommands(status, disk, attach, execute, cifs, complete, get_pubkey, exit))] -pub fn setup() -> Result<(), Error> { - Ok(()) +use crate::progress::{FullProgress, PhaseProgressTrackerHandle}; +use crate::rpc_continuations::Guid; +use crate::util::crypto::EncryptedWire; +use crate::util::io::{create_file, dir_copy, dir_size, Counter}; +use crate::util::Invoke; +use crate::{Error, ErrorKind, ResultExt, DATA_DIR, MAIN_DATA, PACKAGE_DATA}; + +pub fn setup() -> ParentHandler { + ParentHandler::new() + .subcommand( + "status", + from_fn_async(status) + .with_metadata("authenticated", Value::Bool(false)) + .no_cli(), + ) + .subcommand("disk", disk::()) + .subcommand("attach", from_fn_async(attach).no_cli()) + .subcommand("execute", from_fn_async(execute).no_cli()) + .subcommand("cifs", cifs::()) + .subcommand("complete", from_fn_async(complete).no_cli()) + .subcommand( + "get-pubkey", + from_fn_async(get_pubkey) + .with_metadata("authenticated", Value::Bool(false)) + .no_cli(), + ) + .subcommand("exit", from_fn_async(exit).no_cli()) } -#[command(subcommands(list_disks))] -pub fn disk() -> Result<(), Error> { - Ok(()) +pub fn disk() -> ParentHandler { + ParentHandler::new().subcommand( + "list", + from_fn_async(list_disks) + .with_metadata("authenticated", Value::Bool(false)) + .no_cli(), + ) } -#[command(rename = "list", rpc_only, metadata(authenticated = false))] -pub async fn list_disks(#[context] ctx: SetupContext) -> Result, Error> { +pub async fn list_disks(ctx: SetupContext) -> Result, Error> { crate::disk::util::list(&ctx.os_partitions).await } async fn setup_init( ctx: &SetupContext, password: Option, -) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let InitResult { secret_store, db } = - init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; - let mut secrets_handle = secret_store.acquire().await?; - let mut secrets_tx = secrets_handle.begin().await?; - - let mut account = AccountInfo::load(secrets_tx.as_mut()).await?; - - if let Some(password) = password { - account.set_password(&password)?; - account.save(secrets_tx.as_mut()).await?; - db.mutate(|m| { - m.as_server_info_mut() + init_phases: InitPhases, +) -> Result<(AccountInfo, InitResult), Error> { + let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?; + + let account = init_result + .net_ctrl + .db + .mutate(|m| { + let mut account = AccountInfo::load(m)?; + if let Some(password) = password { + account.set_password(&password)?; + } + account.save(m)?; + m.as_public_mut() + .as_server_info_mut() .as_password_hash_mut() - .ser(&account.password) + .ser(&account.password)?; + Ok(account) }) .await?; - } - secrets_tx.commit().await?; + Ok((account, init_result)) +} - Ok(( - account.hostname, - account.key.tor_address(), - account.root_ca_cert, - )) +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AttachParams { + #[serde(rename = "startOsPassword")] + password: Option, + guid: Arc, } -#[command(rpc_only)] pub async fn attach( - #[context] ctx: SetupContext, - #[arg] guid: Arc, - #[arg(rename = "embassy-password")] password: Option, -) -> Result<(), Error> { - let mut status = ctx.setup_status.write().await; - if status.is_some() { - return Err(Error::new( - eyre!("Setup already in progress"), - ErrorKind::InvalidRequest, - )); - } - *status = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); - drop(status); - tokio::task::spawn(async move { - if let Err(e) = async { + ctx: SetupContext, + AttachParams { + password, + guid: disk_guid, + }: AttachParams, +) -> Result { + let setup_ctx = ctx.clone(); + ctx.run_setup(|| async move { + let progress = &setup_ctx.progress; + let mut disk_phase = progress.add_phase("Opening data drive".into(), Some(10)); + let init_phases = InitPhases::new(&progress); + let rpc_ctx_phases = InitRpcContextPhases::new(&progress); + let password: Option = match password { - Some(a) => match a.decrypt(&*ctx) { + Some(a) => match a.decrypt(&setup_ctx) { a @ Some(_) => a, None => { return Err(Error::new( @@ -115,15 +138,17 @@ pub async fn attach( }, None => None, }; + + disk_phase.start(); let requires_reboot = crate::disk::main::import( - &*guid, - &ctx.datadir, + &*disk_guid, + DATA_DIR, if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { RepairStrategy::Aggressive } else { RepairStrategy::Preen }, - if guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) }, + if disk_guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) }, ) .await?; if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { @@ -132,7 +157,7 @@ pub async fn attach( .with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?; } if requires_reboot.0 { - crate::disk::main::export(&*guid, &ctx.datadir).await?; + crate::disk::main::export(&*disk_guid, DATA_DIR).await?; return Err(Error::new( eyre!( "Errors were corrected with your disk, but the server must be restarted in order to proceed" @@ -140,65 +165,85 @@ pub async fn attach( ErrorKind::DiskManagement, )); } - let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; - *ctx.setup_result.write().await = Some((guid, SetupResult { - tor_address: format!("https://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - })); - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: true, - })); - Ok(()) - }.await { - tracing::error!("Error Setting Up Embassy: {}", e); - tracing::debug!("{:?}", e); - *ctx.setup_status.write().await = Some(Err(e.into())); - } - }); - Ok(()) + disk_phase.complete(); + + let (account, net_ctrl) = setup_init(&setup_ctx, password, init_phases).await?; + + let rpc_ctx = RpcContext::init(&setup_ctx.webserver, &setup_ctx.config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok(((&account).try_into()?, rpc_ctx)) + })?; + + Ok(ctx.progress().await) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[serde(tag = "status")] +pub enum SetupStatusRes { + Complete(SetupResult), + Running(SetupProgress), } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct SetupStatus { - pub bytes_transferred: u64, - pub total_bytes: Option, - pub complete: bool, +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetupProgress { + pub progress: FullProgress, + pub guid: Guid, } -#[command(rpc_only, metadata(authenticated = false))] -pub async fn status(#[context] ctx: SetupContext) -> Result, RpcError> { - ctx.setup_status.read().await.clone().transpose() +pub async fn status(ctx: SetupContext) -> Result, Error> { + if let Some(res) = ctx.result.get() { + match res { + Ok((res, _)) => Ok(Some(SetupStatusRes::Complete(res.clone()))), + Err(e) => Err(e.clone_output()), + } + } else { + if ctx.task.initialized() { + Ok(Some(SetupStatusRes::Running(ctx.progress().await))) + } else { + Ok(None) + } + } } /// We want to be able to get a secret, a shared private key with the frontend /// This way the frontend can send a secret, like the password for the setup/ recovory /// without knowing the password over clearnet. We use the public key shared across the network /// since it is fine to share the public, and encrypt against the public. -#[command(rename = "get-pubkey", rpc_only, metadata(authenticated = false))] -pub async fn get_pubkey(#[context] ctx: SetupContext) -> Result { - let secret = ctx.as_ref().clone(); +pub async fn get_pubkey(ctx: SetupContext) -> Result { + let secret = AsRef::::as_ref(&ctx).clone(); let pub_key = secret.to_public_key()?; Ok(pub_key) } -#[command(subcommands(verify_cifs))] -pub fn cifs() -> Result<(), Error> { - Ok(()) +pub fn cifs() -> ParentHandler { + ParentHandler::new().subcommand("verify", from_fn_async(verify_cifs).no_cli()) +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct VerifyCifsParams { + hostname: String, + path: PathBuf, + username: String, + password: Option, } -#[command(rename = "verify", rpc_only)] +// #[command(rename = "verify", rpc_only)] pub async fn verify_cifs( - #[context] ctx: SetupContext, - #[arg] hostname: String, - #[arg] path: PathBuf, - #[arg] username: String, - #[arg] password: Option, -) -> Result { - let password: Option = password.map(|x| x.decrypt(&*ctx)).flatten(); + ctx: SetupContext, + VerifyCifsParams { + hostname, + path, + username, + password, + }: VerifyCifsParams, +) -> Result, Error> { + let password: Option = password.map(|x| x.decrypt(&ctx)).flatten(); let guard = TmpMountGuard::mount( &Cifs { hostname, @@ -209,128 +254,110 @@ pub async fn verify_cifs( ReadWrite, ) .await?; - let embassy_os = recovery_info(&guard).await?; + let start_os = recovery_info(guard.path()).await?; guard.unmount().await?; - embassy_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound)) + if start_os.is_empty() { + return Err(Error::new( + eyre!("No Backup Found"), + crate::ErrorKind::NotFound, + )); + } + Ok(start_os) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize, TS)] #[serde(tag = "type")] -#[serde(rename_all = "kebab-case")] -pub enum RecoverySource { - Migrate { guid: String }, - Backup { target: BackupTargetFS }, +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] +pub enum RecoverySource { + Migrate { + guid: String, + }, + Backup { + target: BackupTargetFS, + password: Password, + server_id: String, + }, +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetupExecuteParams { + start_os_logicalname: PathBuf, + start_os_password: EncryptedWire, + recovery_source: Option>, } -#[command(rpc_only)] +// #[command(rpc_only)] pub async fn execute( - #[context] ctx: SetupContext, - #[arg(rename = "embassy-logicalname")] embassy_logicalname: PathBuf, - #[arg(rename = "embassy-password")] embassy_password: EncryptedWire, - #[arg(rename = "recovery-source")] recovery_source: Option, - #[arg(rename = "recovery-password")] recovery_password: Option, -) -> Result<(), Error> { - let embassy_password = match embassy_password.decrypt(&*ctx) { + ctx: SetupContext, + SetupExecuteParams { + start_os_logicalname, + start_os_password, + recovery_source, + }: SetupExecuteParams, +) -> Result { + let start_os_password = match start_os_password.decrypt(&ctx) { Some(a) => a, None => { return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode embassy-password"), + color_eyre::eyre::eyre!("Couldn't decode startOsPassword"), crate::ErrorKind::Unknown, )) } }; - let recovery_password: Option = match recovery_password { - Some(a) => match a.decrypt(&*ctx) { - Some(a) => Some(a), - None => { - return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode recovery-password"), + let recovery = match recovery_source { + Some(RecoverySource::Backup { + target, + password, + server_id, + }) => Some(RecoverySource::Backup { + target, + password: password.decrypt(&ctx).ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("Couldn't decode recoveryPassword"), crate::ErrorKind::Unknown, - )) - } - }, + ) + })?, + server_id, + }), + Some(RecoverySource::Migrate { guid }) => Some(RecoverySource::Migrate { guid }), None => None, }; - let mut status = ctx.setup_status.write().await; - if status.is_some() { - return Err(Error::new( - eyre!("Setup already in progress"), - ErrorKind::InvalidRequest, - )); - } - *status = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); - drop(status); - tokio::task::spawn({ - async move { - let ctx = ctx.clone(); - let recovery_source = recovery_source; - - let embassy_password = embassy_password; - let recovery_source = recovery_source; - let recovery_password = recovery_password; - match execute_inner( - ctx.clone(), - embassy_logicalname, - embassy_password, - recovery_source, - recovery_password, - ) - .await - { - Ok((guid, hostname, tor_addr, root_ca)) => { - tracing::info!("Setup Complete!"); - *ctx.setup_result.write().await = Some(( - guid, - SetupResult { - tor_address: format!("https://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8( - root_ca.to_pem().expect("failed to serialize root ca"), - ) - .expect("invalid pem string"), - }, - )); - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: true, - })); - } - Err(e) => { - tracing::error!("Error Setting Up Server: {}", e); - tracing::debug!("{:?}", e); - *ctx.setup_status.write().await = Some(Err(e.into())); - } - } - } - }); - Ok(()) + + let setup_ctx = ctx.clone(); + ctx.run_setup(|| execute_inner(setup_ctx, start_os_logicalname, start_os_password, recovery))?; + + Ok(ctx.progress().await) } #[instrument(skip_all)] -#[command(rpc_only)] -pub async fn complete(#[context] ctx: SetupContext) -> Result { - let (guid, setup_result) = if let Some((guid, setup_result)) = &*ctx.setup_result.read().await { - (guid.clone(), setup_result.clone()) - } else { - return Err(Error::new( +// #[command(rpc_only)] +pub async fn complete(ctx: SetupContext) -> Result { + match ctx.result.get() { + Some(Ok((res, ctx))) => { + let mut guid_file = create_file("/media/startos/config/disk.guid").await?; + guid_file.write_all(ctx.disk_guid.as_bytes()).await?; + guid_file.sync_all().await?; + Command::new("systemd-firstboot") + .arg("--root=/media/startos/config/overlay/") + .arg(format!("--hostname={}", res.hostname.0)) + .invoke(ErrorKind::ParseSysInfo) + .await?; + Ok(res.clone()) + } + Some(Err(e)) => Err(e.clone_output()), + None => Err(Error::new( eyre!("setup.execute has not completed successfully"), crate::ErrorKind::InvalidRequest, - )); - }; - let mut guid_file = File::create("/media/embassy/config/disk.guid").await?; - guid_file.write_all(guid.as_bytes()).await?; - guid_file.sync_all().await?; - Ok(setup_result) + )), + } } #[instrument(skip_all)] -#[command(rpc_only)] -pub async fn exit(#[context] ctx: SetupContext) -> Result<(), Error> { +// #[command(rpc_only)] +pub async fn exit(ctx: SetupContext) -> Result<(), Error> { ctx.shutdown.send(()).expect("failed to shutdown"); Ok(()) } @@ -338,11 +365,25 @@ pub async fn exit(#[context] ctx: SetupContext) -> Result<(), Error> { #[instrument(skip_all)] pub async fn execute_inner( ctx: SetupContext, - embassy_logicalname: PathBuf, - embassy_password: String, - recovery_source: Option, - recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { + start_os_logicalname: PathBuf, + start_os_password: String, + recovery_source: Option>, +) -> Result<(SetupResult, RpcContext), Error> { + let progress = &ctx.progress; + let mut disk_phase = progress.add_phase("Formatting data drive".into(), Some(10)); + let restore_phase = match recovery_source.as_ref() { + Some(RecoverySource::Backup { .. }) => { + Some(progress.add_phase("Restoring backup".into(), Some(100))) + } + Some(RecoverySource::Migrate { .. }) => { + Some(progress.add_phase("Transferring data".into(), Some(100))) + } + None => None, + }; + let init_phases = InitPhases::new(&progress); + let rpc_ctx_phases = InitRpcContextPhases::new(&progress); + + disk_phase.start(); let encryption_password = if ctx.disable_encryption { None } else { @@ -350,84 +391,123 @@ pub async fn execute_inner( }; let guid = Arc::new( crate::disk::main::create( - &[embassy_logicalname], + &[start_os_logicalname], &pvscan().await?, - &ctx.datadir, + DATA_DIR, encryption_password, ) .await?, ); - let _ = crate::disk::main::import( - &*guid, - &ctx.datadir, - RepairStrategy::Preen, - encryption_password, - ) - .await?; + let _ = crate::disk::main::import(&*guid, DATA_DIR, RepairStrategy::Preen, encryption_password) + .await?; + disk_phase.complete(); - if let Some(RecoverySource::Backup { target }) = recovery_source { - recover(ctx, guid, embassy_password, target, recovery_password).await - } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { - migrate(ctx, guid, &old_guid, embassy_password).await - } else { - let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; - Ok((guid, hostname, tor_addr, root_ca)) + let progress = SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }; + + match recovery_source { + Some(RecoverySource::Backup { + target, + password, + server_id, + }) => { + recover( + &ctx, + guid, + start_os_password, + target, + server_id, + password, + progress, + ) + .await + } + Some(RecoverySource::Migrate { guid: old_guid }) => { + migrate(&ctx, guid, &old_guid, start_os_password, progress).await + } + None => fresh_setup(&ctx, guid, &start_os_password, progress).await, } } +pub struct SetupExecuteProgress { + pub init_phases: InitPhases, + pub restore_phase: Option, + pub rpc_ctx_phases: InitRpcContextPhases, +} + async fn fresh_setup( ctx: &SetupContext, - embassy_password: &str, -) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let account = AccountInfo::new(embassy_password, root_ca_start_time().await?)?; - let sqlite_pool = ctx.secret_store().await?; - account.save(&sqlite_pool).await?; - sqlite_pool.close().await; - let InitResult { secret_store, .. } = - init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; - secret_store.close().await; - Ok(( - account.hostname.clone(), - account.key.tor_address(), - account.root_ca_cert.clone(), - )) + guid: Arc, + start_os_password: &str, + SetupExecuteProgress { + init_phases, + rpc_ctx_phases, + .. + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { + let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?; + let db = ctx.db().await?; + db.put(&ROOT, &Database::init(&account)?).await?; + drop(db); + + let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?; + + let rpc_ctx = RpcContext::init( + &ctx.webserver, + &ctx.config, + guid, + Some(init_result), + rpc_ctx_phases, + ) + .await?; + + Ok(((&account).try_into()?, rpc_ctx)) } #[instrument(skip_all)] async fn recover( - ctx: SetupContext, + ctx: &SetupContext, guid: Arc, - embassy_password: String, + start_os_password: String, recovery_source: BackupTargetFS, - recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { + server_id: String, + recovery_password: String, + progress: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; recover_full_embassy( ctx, guid.clone(), - embassy_password, + start_os_password, recovery_source, - recovery_password, + &server_id, + &recovery_password, + progress, ) .await } #[instrument(skip_all)] async fn migrate( - ctx: SetupContext, + ctx: &SetupContext, guid: Arc, old_guid: &str, - embassy_password: String, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); - + start_os_password: String, + SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { + let mut restore_phase = restore_phase.or_not_found("restore progress")?; + + restore_phase.start(); let _ = crate::disk::main::import( &old_guid, - "/media/embassy/migrate", + "/media/startos/migrate", RepairStrategy::Preen, if guid.ends_with("_UNENC") { None @@ -437,10 +517,10 @@ async fn migrate( ) .await?; - let main_transfer_args = ("/media/embassy/migrate/main/", "/embassy-data/main/"); + let main_transfer_args = ("/media/startos/migrate/main/", formatcp!("{MAIN_DATA}/")); let package_data_transfer_args = ( - "/media/embassy/migrate/package-data/", - "/embassy-data/package-data/", + "/media/startos/migrate/package-data/", + formatcp!("{PACKAGE_DATA}/"), ); let tmpdir = Path::new(package_data_transfer_args.0).join("tmp"); @@ -464,20 +544,12 @@ async fn migrate( res = async { loop { tokio::time::sleep(Duration::from_secs(1)).await; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: Some(main_transfer_size.load() + package_data_transfer_size.load()), - complete: false, - })); + restore_phase.set_total(main_transfer_size.load() + package_data_transfer_size.load()); } } => res, }; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: Some(size), - complete: false, - })); + restore_phase.set_total(size); let main_transfer_progress = Counter::new(0, ordering); let package_data_transfer_progress = Counter::new(0, ordering); @@ -493,18 +565,24 @@ async fn migrate( res = async { loop { tokio::time::sleep(Duration::from_secs(1)).await; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: main_transfer_progress.load() + package_data_transfer_progress.load(), - total_bytes: Some(size), - complete: false, - })); + restore_phase.set_done(main_transfer_progress.load() + package_data_transfer_progress.load()); } } => res, } - let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?; + crate::disk::main::export(&old_guid, "/media/startos/migrate").await?; + restore_phase.complete(); + + let (account, net_ctrl) = setup_init(&ctx, Some(start_os_password), init_phases).await?; - crate::disk::main::export(&old_guid, "/media/embassy/migrate").await?; + let rpc_ctx = RpcContext::init( + &ctx.webserver, + &ctx.config, + guid, + Some(net_ctrl), + rpc_ctx_phases, + ) + .await?; - Ok((guid, hostname, tor_addr, root_ca)) + Ok(((&account).try_into()?, rpc_ctx)) } diff --git a/core/startos/src/shutdown.rs b/core/startos/src/shutdown.rs index e5ff969b6..4e45f74c0 100644 --- a/core/startos/src/shutdown.rs +++ b/core/startos/src/shutdown.rs @@ -1,16 +1,13 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; -use rpc_toolkit::command; - use crate::context::RpcContext; use crate::disk::main::export; use crate::init::{STANDBY_MODE_PATH, SYSTEM_REBUILD_PATH}; use crate::prelude::*; use crate::sound::SHUTDOWN; -use crate::util::docker::CONTAINER_TOOL; -use crate::util::{display_none, Invoke}; -use crate::PLATFORM; +use crate::util::Invoke; +use crate::{DATA_DIR, PLATFORM}; #[derive(Debug, Clone)] pub struct Shutdown { @@ -44,28 +41,6 @@ impl Shutdown { tracing::error!("Error Stopping Journald: {}", e); tracing::debug!("{:?}", e); } - if CONTAINER_TOOL == "docker" { - if let Err(e) = Command::new("systemctl") - .arg("stop") - .arg("docker") - .invoke(crate::ErrorKind::Docker) - .await - { - tracing::error!("Error Stopping Docker: {}", e); - tracing::debug!("{:?}", e); - } - } else if CONTAINER_TOOL == "podman" { - if let Err(e) = Command::new("podman") - .arg("rm") - .arg("-f") - .arg("netdummy") - .invoke(crate::ErrorKind::Docker) - .await - { - tracing::error!("Error Stopping Podman: {}", e); - tracing::debug!("{:?}", e); - } - } if let Some((guid, datadir)) = &self.export_args { if let Err(e) = export(guid, datadir).await { tracing::error!("Error Exporting Volume Group: {}", e); @@ -100,11 +75,11 @@ impl Shutdown { } } -#[command(display(display_none))] -pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> { +pub async fn shutdown(ctx: RpcContext) -> Result<(), Error> { ctx.db .mutate(|db| { - db.as_server_info_mut() + db.as_public_mut() + .as_server_info_mut() .as_status_info_mut() .as_shutting_down_mut() .ser(&true) @@ -112,7 +87,7 @@ pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> { .await?; ctx.shutdown .send(Some(Shutdown { - export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())), + export_args: Some((ctx.disk_guid.clone(), Path::new(DATA_DIR).to_owned())), restart: false, })) .map_err(|_| ()) @@ -120,11 +95,11 @@ pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> { Ok(()) } -#[command(display(display_none))] -pub async fn restart(#[context] ctx: RpcContext) -> Result<(), Error> { +pub async fn restart(ctx: RpcContext) -> Result<(), Error> { ctx.db .mutate(|db| { - db.as_server_info_mut() + db.as_public_mut() + .as_server_info_mut() .as_status_info_mut() .as_restarting_mut() .ser(&true) @@ -132,7 +107,7 @@ pub async fn restart(#[context] ctx: RpcContext) -> Result<(), Error> { .await?; ctx.shutdown .send(Some(Shutdown { - export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())), + export_args: Some((ctx.disk_guid.clone(), Path::new(DATA_DIR).to_owned())), restart: true, })) .map_err(|_| ()) @@ -140,8 +115,7 @@ pub async fn restart(#[context] ctx: RpcContext) -> Result<(), Error> { Ok(()) } -#[command(display(display_none))] -pub async fn rebuild(#[context] ctx: RpcContext) -> Result<(), Error> { +pub async fn rebuild(ctx: RpcContext) -> Result<(), Error> { tokio::fs::write(SYSTEM_REBUILD_PATH, b"").await?; restart(ctx).await } diff --git a/core/startos/src/sound.rs b/core/startos/src/sound.rs index 8dc78357c..8cedd78ce 100644 --- a/core/startos/src/sound.rs +++ b/core/startos/src/sound.rs @@ -10,12 +10,12 @@ use crate::util::{FileLock, Invoke}; use crate::{Error, ErrorKind}; lazy_static::lazy_static! { - static ref SEMITONE_K: f64 = 2f64.powf(1f64 / 12f64); - static ref A_4: f64 = 440f64; - static ref C_0: f64 = *A_4 / SEMITONE_K.powf(9f64) / 2f64.powf(4f64); + static ref SEMITONE_K: f64 = 2f64.powf(1.0 / 12.0); + static ref A_4: f64 = 440.0; + static ref C_0: f64 = *A_4 / SEMITONE_K.powf(9.0) / 2_f64.powf(4.0); } -pub const SOUND_LOCK_FILE: &str = "/etc/embassy/sound.lock"; +pub const SOUND_LOCK_FILE: &str = "/run/startos/sound.lock"; struct SoundInterface { guard: Option, diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index 697e05727..0c52fa136 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -1,31 +1,69 @@ +use std::collections::BTreeMap; use std::path::Path; -use chrono::Utc; -use clap::ArgMatches; -use color_eyre::eyre::eyre; -use rpc_toolkit::command; -use sqlx::{Pool, Postgres}; +use clap::builder::ValueParserFactory; +use clap::Parser; +use imbl_value::InternedString; +use models::FromStrParser; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tokio::fs::OpenOptions; +use tokio::process::Command; use tracing::instrument; +use ts_rs::TS; -use crate::context::RpcContext; -use crate::util::display_none; -use crate::util::serde::{display_serializable, IoFormat}; -use crate::{Error, ErrorKind}; +use crate::context::{CliContext, RpcContext}; +use crate::hostname::Hostname; +use crate::prelude::*; +use crate::util::io::create_file; +use crate::util::serde::{display_serializable, HandlerExtSerde, Pem, WithIoFormat}; +use crate::util::Invoke; -static SSH_AUTHORIZED_KEYS_FILE: &str = "/home/start9/.ssh/authorized_keys"; +pub const SSH_DIR: &str = "/home/start9/.ssh"; -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct PubKey( +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SshKeys(BTreeMap>); +impl SshKeys { + pub fn new() -> Self { + Self(BTreeMap::new()) + } +} + +impl From>> for SshKeys { + fn from(map: BTreeMap>) -> Self { + Self(map) + } +} +impl Map for SshKeys { + type Key = InternedString; + type Value = WithTimeData; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(type = "string")] +pub struct SshPubKey( #[serde(serialize_with = "crate::util::serde::serialize_display")] #[serde(deserialize_with = "crate::util::serde::deserialize_from_str")] - openssh_keys::PublicKey, + pub openssh_keys::PublicKey, ); +impl ValueParserFactory for SshPubKey { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} #[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct SshKeyResponse { pub alg: String, - pub fingerprint: String, + pub fingerprint: InternedString, pub hostname: String, pub created_at: String, } @@ -39,10 +77,10 @@ impl std::fmt::Display for SshKeyResponse { } } -impl std::str::FromStr for PubKey { +impl std::str::FromStr for SshPubKey { type Err = Error; fn from_str(s: &str) -> Result { - s.parse().map(|pk| PubKey(pk)).map_err(|e| Error { + s.parse().map(|pk| SshPubKey(pk)).map_err(|e| Error { source: e.into(), kind: crate::ErrorKind::ParseSshKey, revision: None, @@ -50,75 +88,104 @@ impl std::str::FromStr for PubKey { } } -#[command(subcommands(add, delete, list,))] -pub fn ssh() -> Result<(), Error> { - Ok(()) +// #[command(subcommands(add, delete, list,))] +pub fn ssh() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add) + .no_display() + .with_about("Add ssh key") + .with_call_remote::(), + ) + .subcommand( + "delete", + from_fn_async(delete) + .no_display() + .with_about("Remove ssh key") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_all_ssh_keys(handle.params, result)) + }) + .with_about("List ssh keys") + .with_call_remote::(), + ) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct AddParams { + key: SshPubKey, } -#[command(display(display_none))] #[instrument(skip_all)] -pub async fn add(#[context] ctx: RpcContext, #[arg] key: PubKey) -> Result { - let pool = &ctx.secret_store; - // check fingerprint for duplicates - let fp = key.0.fingerprint_md5(); - match sqlx::query!("SELECT * FROM ssh_keys WHERE fingerprint = $1", fp) - .fetch_optional(pool) - .await? - { - None => { - // if no duplicates, insert into DB - let raw_key = format!("{}", key.0); - let created_at = Utc::now().to_rfc3339(); - sqlx::query!( - "INSERT INTO ssh_keys (fingerprint, openssh_pubkey, created_at) VALUES ($1, $2, $3)", - fp, - raw_key, - created_at - ) - .execute(pool) - .await?; - // insert into live key file, for now we actually do a wholesale replacement of the keys file, for maximum - // consistency - sync_keys_from_db(pool, Path::new(SSH_AUTHORIZED_KEYS_FILE)).await?; - Ok(SshKeyResponse { - alg: key.0.keytype().to_owned(), - fingerprint: fp, - hostname: key.0.comment.unwrap_or(String::new()).to_owned(), - created_at, - }) - } - Some(_) => Err(Error::new(eyre!("Duplicate ssh key"), ErrorKind::Duplicate)), - } +pub async fn add(ctx: RpcContext, AddParams { key }: AddParams) -> Result { + let mut key = WithTimeData::new(key); + let fingerprint = InternedString::intern(key.0.fingerprint_md5()); + let (keys, res) = ctx + .db + .mutate(move |m| { + m.as_private_mut() + .as_ssh_pubkeys_mut() + .insert(&fingerprint, &key)?; + + Ok(( + m.as_private().as_ssh_pubkeys().de()?, + SshKeyResponse { + alg: key.0.keytype().to_owned(), + fingerprint, + hostname: key.0.comment.take().unwrap_or_default(), + created_at: key.created_at.to_rfc3339(), + }, + )) + }) + .await?; + sync_pubkeys(&keys, SSH_DIR).await?; + Ok(res) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DeleteParams { + #[ts(type = "string")] + fingerprint: InternedString, } -#[command(display(display_none))] + #[instrument(skip_all)] -pub async fn delete(#[context] ctx: RpcContext, #[arg] fingerprint: String) -> Result<(), Error> { - let pool = &ctx.secret_store; - // check if fingerprint is in DB - // if in DB, remove it from DB - let n = sqlx::query!("DELETE FROM ssh_keys WHERE fingerprint = $1", fingerprint) - .execute(pool) - .await? - .rows_affected(); - // if not in DB, Err404 - if n == 0 { - Err(Error { - source: color_eyre::eyre::eyre!("SSH Key Not Found"), - kind: crate::error::ErrorKind::NotFound, - revision: None, +pub async fn delete( + ctx: RpcContext, + DeleteParams { fingerprint }: DeleteParams, +) -> Result<(), Error> { + let keys = ctx + .db + .mutate(|m| { + let keys_ref = m.as_private_mut().as_ssh_pubkeys_mut(); + if keys_ref.remove(&fingerprint)?.is_some() { + keys_ref.de() + } else { + Err(Error { + source: color_eyre::eyre::eyre!("SSH Key Not Found"), + kind: crate::error::ErrorKind::NotFound, + revision: None, + }) + } }) - } else { - // AND overlay key file - sync_keys_from_db(pool, Path::new(SSH_AUTHORIZED_KEYS_FILE)).await?; - Ok(()) - } + .await?; + sync_pubkeys(&keys, SSH_DIR).await } -fn display_all_ssh_keys(all: Vec, matches: &ArgMatches) { +fn display_all_ssh_keys(params: WithIoFormat, result: Vec) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(all, matches); + if let Some(format) = params.format { + return display_serializable(format, params); } let mut table = Table::new(); @@ -128,7 +195,7 @@ fn display_all_ssh_keys(all: Vec, matches: &ArgMatches) { "FINGERPRINT", "HOSTNAME", ]); - for key in all { + for key in result { let row = row![ &format!("{}", key.created_at), &key.alg, @@ -140,58 +207,112 @@ fn display_all_ssh_keys(all: Vec, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_all_ssh_keys))] #[instrument(skip_all)] -pub async fn list( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result, Error> { - let pool = &ctx.secret_store; - // list keys in DB and return them - let entries = sqlx::query!("SELECT fingerprint, openssh_pubkey, created_at FROM ssh_keys") - .fetch_all(pool) - .await?; - Ok(entries +pub async fn list(ctx: RpcContext) -> Result, Error> { + ctx.db + .peek() + .await + .into_private() + .into_ssh_pubkeys() + .into_entries()? .into_iter() - .map(|r| { - let k = PubKey(r.openssh_pubkey.parse().unwrap()).0; - let alg = k.keytype().to_owned(); - let fingerprint = k.fingerprint_md5(); - let hostname = k.comment.unwrap_or("".to_owned()); - let created_at = r.created_at; - SshKeyResponse { - alg, + .map(|(fingerprint, key)| { + let mut key = key.de()?; + Ok(SshKeyResponse { + alg: key.0.keytype().to_owned(), fingerprint, - hostname, - created_at, - } + hostname: key.0.comment.take().unwrap_or_default(), + created_at: key.created_at.to_rfc3339(), + }) }) - .collect()) + .collect() } #[instrument(skip_all)] -pub async fn sync_keys_from_db>( - pool: &Pool, - dest: P, +pub async fn sync_keys>( + hostname: &Hostname, + privkey: &Pem, + pubkeys: &SshKeys, + ssh_dir: P, ) -> Result<(), Error> { - let dest = dest.as_ref(); - let keys = sqlx::query!("SELECT openssh_pubkey FROM ssh_keys") - .fetch_all(pool) + use tokio::io::AsyncWriteExt; + + let ssh_dir = ssh_dir.as_ref(); + if tokio::fs::metadata(ssh_dir).await.is_err() { + tokio::fs::create_dir_all(ssh_dir).await?; + } + + let id_alg = if privkey.0.algorithm().is_ed25519() { + "id_ed25519" + } else if privkey.0.algorithm().is_ecdsa() { + "id_ecdsa" + } else if privkey.0.algorithm().is_rsa() { + "id_rsa" + } else { + "id_unknown" + }; + + let privkey_path = ssh_dir.join(id_alg); + let mut f = OpenOptions::new() + .create(true) + .write(true) + .mode(0o600) + .open(&privkey_path) + .await + .with_ctx(|_| { + ( + ErrorKind::Filesystem, + lazy_format!("create {privkey_path:?}"), + ) + })?; + f.write_all(privkey.to_string().as_bytes()).await?; + f.write_all(b"\n").await?; + f.sync_all().await?; + let mut f = create_file(ssh_dir.join(id_alg).with_extension("pub")).await?; + f.write_all( + (privkey + .0 + .public_key() + .to_openssh() + .with_kind(ErrorKind::OpenSsh)? + + " start9@" + + &*hostname.0) + .as_bytes(), + ) + .await?; + f.write_all(b"\n").await?; + f.sync_all().await?; + + let mut f = create_file(ssh_dir.join("authorized_keys")).await?; + for key in pubkeys.0.values() { + f.write_all(key.0.to_key_format().as_bytes()).await?; + f.write_all(b"\n").await?; + } + + Command::new("chown") + .arg("-R") + .arg("start9:startos") + .arg(ssh_dir) + .invoke(ErrorKind::Filesystem) .await?; - let contents: String = keys - .into_iter() - .map(|k| format!("{}\n", k.openssh_pubkey)) - .collect(); - let ssh_dir = dest.parent().ok_or_else(|| { - Error::new( - eyre!("SSH Key File cannot be \"/\""), - crate::ErrorKind::Filesystem, - ) - })?; + + Ok(()) +} + +#[instrument(skip_all)] +pub async fn sync_pubkeys>(pubkeys: &SshKeys, ssh_dir: P) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + let ssh_dir = ssh_dir.as_ref(); if tokio::fs::metadata(ssh_dir).await.is_err() { tokio::fs::create_dir_all(ssh_dir).await?; } - std::fs::write(dest, contents).map_err(|e| e.into()) + + let mut f = create_file(ssh_dir.join("authorized_keys")).await?; + for key in pubkeys.0.values() { + f.write_all(key.0.to_key_format().as_bytes()).await?; + f.write_all(b"\n").await?; + } + + Ok(()) } diff --git a/core/startos/src/status/health_check.rs b/core/startos/src/status/health_check.rs index 1b3e8f6b5..861954d29 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -1,126 +1,99 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; -use chrono::{DateTime, Utc}; +use clap::builder::ValueParserFactory; +use models::FromStrParser; pub use models::HealthCheckId; -use models::ImageId; use serde::{Deserialize, Serialize}; -use tracing::instrument; +use ts_rs::TS; -use crate::context::RpcContext; -use crate::procedure::{NoOutput, PackageProcedure, ProcedureName}; -use crate::s9pk::manifest::PackageId; -use crate::util::serde::Duration; -use crate::util::Version; -use crate::volume::Volumes; -use crate::{Error, ResultExt}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct HealthChecks(pub BTreeMap); -impl HealthChecks { - #[instrument(skip_all)] - pub fn validate( - &self, - eos_version: &Version, - volumes: &Volumes, - image_ids: &BTreeSet, - ) -> Result<(), Error> { - for check in self.0.values() { - check - .implementation - .validate(eos_version, volumes, image_ids, false) - .with_ctx(|_| { - ( - crate::ErrorKind::ValidateS9pk, - format!("Health Check {}", check.name), - ) - })?; - } - Ok(()) - } - pub async fn check_all( - &self, - ctx: &RpcContext, - started: DateTime, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result, Error> { - let res = futures::future::try_join_all(self.0.iter().map(|(id, check)| async move { - Ok::<_, Error>(( - id.clone(), - check - .check(ctx, id, started, pkg_id, pkg_version, volumes) - .await?, - )) - })) - .await?; - Ok(res.into_iter().collect()) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct HealthCheck { +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +pub struct NamedHealthCheckResult { pub name: String, - pub success_message: Option, #[serde(flatten)] - implementation: PackageProcedure, - pub timeout: Option, + pub kind: NamedHealthCheckResultKind, +} +// healthCheckName:kind:message OR healthCheckName:kind +impl FromStr for NamedHealthCheckResult { + type Err = color_eyre::eyre::Report; + fn from_str(s: &str) -> Result { + let from_parts = |name: &str, kind: &str, message: Option<&str>| { + let message = message.map(|x| x.to_string()); + let kind = match kind { + "success" => NamedHealthCheckResultKind::Success { message }, + "disabled" => NamedHealthCheckResultKind::Disabled { message }, + "starting" => NamedHealthCheckResultKind::Starting { message }, + "loading" => NamedHealthCheckResultKind::Loading { + message: message.unwrap_or_default(), + }, + "failure" => NamedHealthCheckResultKind::Failure { + message: message.unwrap_or_default(), + }, + _ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")), + }; + Ok(Self { + name: name.to_string(), + kind, + }) + }; + let parts = s.split(':').collect::>(); + match &*parts { + [name, kind, message] => from_parts(name, kind, Some(message)), + [name, kind] => from_parts(name, kind, None), + _ => Err(color_eyre::eyre::eyre!( + "Could not match the shape of the result ${parts:?}" + )), + } + } } -impl HealthCheck { - #[instrument(skip_all)] - pub async fn check( - &self, - ctx: &RpcContext, - id: &HealthCheckId, - started: DateTime, - pkg_id: &PackageId, - pkg_version: &Version, - volumes: &Volumes, - ) -> Result { - let res = self - .implementation - .execute( - ctx, - pkg_id, - pkg_version, - ProcedureName::Health(id.clone()), - volumes, - Some(Utc::now().signed_duration_since(started).num_milliseconds()), - Some( - self.timeout - .map_or(std::time::Duration::from_secs(30), |d| *d), - ), - ) - .await?; - Ok(match res { - Ok(NoOutput) => HealthCheckResult::Success, - Err((59, _)) => HealthCheckResult::Disabled, - Err((60, _)) => HealthCheckResult::Starting, - Err((61, message)) => HealthCheckResult::Loading { message }, - Err((_, error)) => HealthCheckResult::Failure { error }, - }) +impl ValueParserFactory for NamedHealthCheckResult { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] #[serde(tag = "result")] -pub enum HealthCheckResult { - Success, - Disabled, - Starting, +pub enum NamedHealthCheckResultKind { + Success { message: Option }, + Disabled { message: Option }, + Starting { message: Option }, Loading { message: String }, - Failure { error: String }, + Failure { message: String }, } -impl std::fmt::Display for HealthCheckResult { +impl std::fmt::Display for NamedHealthCheckResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - HealthCheckResult::Success => write!(f, "Succeeded"), - HealthCheckResult::Disabled => write!(f, "Disabled"), - HealthCheckResult::Starting => write!(f, "Starting"), - HealthCheckResult::Loading { message } => write!(f, "Loading ({})", message), - HealthCheckResult::Failure { error } => write!(f, "Failed ({})", error), + let name = &self.name; + match &self.kind { + NamedHealthCheckResultKind::Success { message } => { + if let Some(message) = message { + write!(f, "{name}: Succeeded ({message})") + } else { + write!(f, "{name}: Succeeded") + } + } + NamedHealthCheckResultKind::Disabled { message } => { + if let Some(message) = message { + write!(f, "{name}: Disabled ({message})") + } else { + write!(f, "{name}: Disabled") + } + } + NamedHealthCheckResultKind::Starting { message } => { + if let Some(message) = message { + write!(f, "{name}: Starting ({message})") + } else { + write!(f, "{name}: Starting") + } + } + NamedHealthCheckResultKind::Loading { message } => { + write!(f, "{name}: Loading ({message})") + } + NamedHealthCheckResultKind::Failure { message } => { + write!(f, "{name}: Failed ({message})") + } } } } diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 2a5a9391f..cf1ca0658 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -1,48 +1,43 @@ use std::collections::BTreeMap; use chrono::{DateTime, Utc}; -use models::PackageId; +use imbl::OrdMap; use serde::{Deserialize, Serialize}; +use ts_rs::TS; use self::health_check::HealthCheckId; use crate::prelude::*; -use crate::status::health_check::HealthCheckResult; +use crate::service::start_stop::StartStop; +use crate::status::health_check::NamedHealthCheckResult; pub mod health_check; -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Status { - pub configured: bool, - pub main: MainStatus, - #[serde(default)] - pub dependency_config_errors: DependencyConfigErrors, -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel, Default)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct DependencyConfigErrors(pub BTreeMap); -impl Map for DependencyConfigErrors { - type Key = PackageId; - type Value = String; -} -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(tag = "status")] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)] +#[serde(tag = "main")] +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] pub enum MainStatus { + Error { + on_rebuild: StartStop, + message: String, + debug: Option, + }, Stopped, Restarting, + Restoring, Stopping, - Starting, + Starting { + #[ts(as = "BTreeMap")] + health: OrdMap, + }, Running { + #[ts(type = "string")] started: DateTime, - health: BTreeMap, + #[ts(as = "BTreeMap")] + health: OrdMap, }, BackingUp { - started: Option>, - health: BTreeMap, + on_complete: StartStop, }, } impl MainStatus { @@ -50,45 +45,60 @@ impl MainStatus { match self { MainStatus::Starting { .. } | MainStatus::Running { .. } + | MainStatus::Restarting | MainStatus::BackingUp { - started: Some(_), .. + on_complete: StartStop::Start, + } + | MainStatus::Error { + on_rebuild: StartStop::Start, + .. } => true, MainStatus::Stopped - | MainStatus::Stopping - | MainStatus::Restarting - | MainStatus::BackingUp { started: None, .. } => false, - } - } - pub fn stop(&mut self) { - match self { - MainStatus::Starting { .. } | MainStatus::Running { .. } => { - *self = MainStatus::Stopping; - } - MainStatus::BackingUp { started, .. } => { - *started = None; + | MainStatus::Restoring + | MainStatus::Stopping { .. } + | MainStatus::BackingUp { + on_complete: StartStop::Stop, } - MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => (), + | MainStatus::Error { + on_rebuild: StartStop::Stop, + .. + } => false, } } - pub fn started(&self) -> Option> { - match self { - MainStatus::Running { started, .. } => Some(*started), - MainStatus::BackingUp { started, .. } => *started, - MainStatus::Stopped => None, - MainStatus::Restarting => None, - MainStatus::Stopping => None, - MainStatus::Starting { .. } => None, + + pub fn major_changes(&self, other: &Self) -> bool { + match (self, other) { + (MainStatus::Running { .. }, MainStatus::Running { .. }) => false, + (MainStatus::Starting { .. }, MainStatus::Starting { .. }) => false, + (MainStatus::Stopping, MainStatus::Stopping) => false, + (MainStatus::Stopped, MainStatus::Stopped) => false, + (MainStatus::Restarting, MainStatus::Restarting) => false, + (MainStatus::Restoring, MainStatus::Restoring) => false, + (MainStatus::BackingUp { .. }, MainStatus::BackingUp { .. }) => false, + (MainStatus::Error { .. }, MainStatus::Error { .. }) => false, + _ => true, } } + pub fn backing_up(&self) -> Self { - let (started, health) = match self { - MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()), - MainStatus::Running { started, health } => (Some(started.clone()), health.clone()), - MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { - (None, Default::default()) - } - MainStatus::BackingUp { .. } => return self.clone(), - }; - MainStatus::BackingUp { started, health } + MainStatus::BackingUp { + on_complete: if self.running() { + StartStop::Start + } else { + StartStop::Stop + }, + } + } + + pub fn health(&self) -> Option<&OrdMap> { + match self { + MainStatus::Running { health, .. } | MainStatus::Starting { health } => Some(health), + MainStatus::BackingUp { .. } + | MainStatus::Stopped + | MainStatus::Restoring + | MainStatus::Stopping { .. } + | MainStatus::Restarting + | MainStatus::Error { .. } => None, + } } } diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index b5cd42844..123fdec42 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -1,34 +1,56 @@ use std::collections::BTreeSet; use std::fmt; +use std::sync::Arc; use chrono::Utc; -use clap::ArgMatches; +use clap::Parser; use color_eyre::eyre::eyre; use futures::FutureExt; -use rpc_toolkit::command; -use rpc_toolkit::yajrc::RpcError; +use imbl::vector; +use mail_send::mail_builder::{self, MessageBuilder}; +use mail_send::SmtpClientBuilder; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; +use rustls::crypto::CryptoProvider; +use rustls::RootCertStore; +use rustls_pki_types::CertificateDer; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::process::Command; use tokio::sync::broadcast::Receiver; use tokio::sync::RwLock; use tracing::instrument; +use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::disk::util::{get_available, get_used}; -use crate::logs::{ - cli_logs_generic_follow, cli_logs_generic_nofollow, fetch_logs, follow_logs, LogFollowResponse, - LogResponse, LogSource, -}; +use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT}; use crate::prelude::*; +use crate::rpc_continuations::RpcContinuations; use crate::shutdown::Shutdown; use crate::util::cpupower::{get_available_governors, set_governor, Governor}; -use crate::util::serde::{display_serializable, IoFormat}; -use crate::util::{display_none, Invoke}; -use crate::{Error, ErrorKind, ResultExt}; - -#[command(subcommands(zram, governor))] -pub async fn experimental() -> Result<(), Error> { - Ok(()) +use crate::util::io::open_file; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; +use crate::util::Invoke; +use crate::{MAIN_DATA, PACKAGE_DATA}; + +pub fn experimental() -> ParentHandler { + ParentHandler::new() + .subcommand( + "zram", + from_fn_async(zram) + .no_display() + .with_about("Enable zram") + .with_call_remote::(), + ) + .subcommand( + "governor", + from_fn_async(governor) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_governor_info(handle.params, result)) + }) + .with_about("Show current and available CPU governors") + .with_call_remote::(), + ) } pub async fn enable_zram() -> Result<(), Error> { @@ -59,11 +81,17 @@ pub async fn enable_zram() -> Result<(), Error> { Ok(()) } -#[command(display(display_none))] -pub async fn zram(#[context] ctx: RpcContext, #[arg] enable: bool) -> Result<(), Error> { +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ZramParams { + enable: bool, +} + +pub async fn zram(ctx: RpcContext, ZramParams { enable }: ZramParams) -> Result<(), Error> { let db = ctx.db.peek().await; - let zram = db.as_server_info().as_zram().de()?; + let zram = db.as_public().as_server_info().as_zram().de()?; if enable == zram { return Ok(()); } @@ -80,7 +108,10 @@ pub async fn zram(#[context] ctx: RpcContext, #[arg] enable: bool) -> Result<(), } ctx.db .mutate(|v| { - v.as_server_info_mut().as_zram_mut().ser(&enable)?; + v.as_public_mut() + .as_server_info_mut() + .as_zram_mut() + .ser(&enable)?; Ok(()) }) .await?; @@ -93,17 +124,17 @@ pub struct GovernorInfo { available: BTreeSet, } -fn display_governor_info(arg: GovernorInfo, matches: &ArgMatches) { +fn display_governor_info(params: WithIoFormat, result: GovernorInfo) { use prettytable::*; - if matches.is_present("format") { - return display_serializable(arg, matches); + if let Some(format) = params.format { + return display_serializable(format, params); } let mut table = Table::new(); table.add_row(row![bc -> "GOVERNORS"]); - for entry in arg.available { - if Some(&entry) == arg.current.as_ref() { + for entry in result.available { + if Some(&entry) == result.current.as_ref() { table.add_row(row![g -> format!("* {entry} (current)")]); } else { table.add_row(row![entry]); @@ -112,13 +143,16 @@ fn display_governor_info(arg: GovernorInfo, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_governor_info))] +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct GovernorParams { + set: Option, +} + pub async fn governor( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, - #[arg] set: Option, + ctx: RpcContext, + GovernorParams { set, .. }: GovernorParams, ) -> Result { let available = get_available_governors().await?; if let Some(set) = set { @@ -130,10 +164,22 @@ pub async fn governor( } set_governor(&set).await?; ctx.db - .mutate(|d| d.as_server_info_mut().as_governor_mut().ser(&Some(set))) + .mutate(|d| { + d.as_public_mut() + .as_server_info_mut() + .as_governor_mut() + .ser(&Some(set)) + }) .await?; } - let current = ctx.db.peek().await.as_server_info().as_governor().de()?; + let current = ctx + .db + .peek() + .await + .as_public() + .as_server_info() + .as_governor() + .de()?; Ok(GovernorInfo { current, available }) } @@ -143,13 +189,13 @@ pub struct TimeInfo { uptime: u64, } -fn display_time(arg: TimeInfo, matches: &ArgMatches) { +pub fn display_time(params: WithIoFormat, arg: TimeInfo) { use std::fmt::Write; use prettytable::*; - if matches.is_present("format") { - return display_serializable(arg, matches); + if let Some(format) = params.format { + return display_serializable(format, arg); } let days = arg.uptime / (24 * 60 * 60); @@ -185,118 +231,19 @@ fn display_time(arg: TimeInfo, matches: &ArgMatches) { table.print_tty(false).unwrap(); } -#[command(display(display_time))] -pub async fn time( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { +pub async fn time(ctx: RpcContext, _: Empty) -> Result { Ok(TimeInfo { now: Utc::now().to_rfc3339(), uptime: ctx.start_time.elapsed().as_secs(), }) } -#[command( - custom_cli(cli_logs(async, context(CliContext))), - subcommands(self(logs_nofollow(async)), logs_follow), - display(display_none) -)] -pub async fn logs( - #[arg(short = 'l', long = "limit")] limit: Option, - #[arg(short = 'c', long = "cursor")] cursor: Option, - #[arg(short = 'B', long = "before", default)] before: bool, - #[arg(short = 'f', long = "follow", default)] follow: bool, -) -> Result<(Option, Option, bool, bool), Error> { - Ok((limit, cursor, before, follow)) -} -pub async fn cli_logs( - ctx: CliContext, - (limit, cursor, before, follow): (Option, Option, bool, bool), -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "server.logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "server.logs", None, limit, cursor, before).await - } -} -pub async fn logs_nofollow( - _ctx: (), - (limit, cursor, before, _): (Option, Option, bool, bool), -) -> Result { - fetch_logs(LogSource::System, limit, cursor, before).await -} - -#[command(rpc_only, rename = "follow", display(display_none))] -pub async fn logs_follow( - #[context] ctx: RpcContext, - #[parent_data] (limit, _, _, _): (Option, Option, bool, bool), -) -> Result { - follow_logs(ctx, LogSource::System, limit).await -} - -#[command( - rename = "kernel-logs", - custom_cli(cli_kernel_logs(async, context(CliContext))), - subcommands(self(kernel_logs_nofollow(async)), kernel_logs_follow), - display(display_none) -)] -pub async fn kernel_logs( - #[arg(short = 'l', long = "limit")] limit: Option, - #[arg(short = 'c', long = "cursor")] cursor: Option, - #[arg(short = 'B', long = "before", default)] before: bool, - #[arg(short = 'f', long = "follow", default)] follow: bool, -) -> Result<(Option, Option, bool, bool), Error> { - Ok((limit, cursor, before, follow)) -} -pub async fn cli_kernel_logs( - ctx: CliContext, - (limit, cursor, before, follow): (Option, Option, bool, bool), -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "server.kernel-logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "server.kernel-logs", None, limit, cursor, before).await - } -} -pub async fn kernel_logs_nofollow( - _ctx: (), - (limit, cursor, before, _): (Option, Option, bool, bool), -) -> Result { - fetch_logs(LogSource::Kernel, limit, cursor, before).await +pub fn logs>() -> ParentHandler { + crate::logs::logs(|_: &C, _| async { Ok(LogSource::Unit(SYSTEM_UNIT)) }) } -#[command(rpc_only, rename = "follow", display(display_none))] -pub async fn kernel_logs_follow( - #[context] ctx: RpcContext, - #[parent_data] (limit, _, _, _): (Option, Option, bool, bool), -) -> Result { - follow_logs(ctx, LogSource::Kernel, limit).await +pub fn kernel_logs>() -> ParentHandler { + crate::logs::logs(|_: &C, _| async { Ok(LogSource::Kernel) }) } #[derive(Serialize, Deserialize)] @@ -412,12 +359,12 @@ impl<'de> Deserialize<'de> for GigaBytes { } #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct MetricsGeneral { pub temperature: Option, } #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct MetricsMemory { pub percentage_used: Percentage, pub total: MebiBytes, @@ -428,7 +375,7 @@ pub struct MetricsMemory { pub zram_used: MebiBytes, } #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct MetricsCpu { percentage_used: Percentage, idle: Percentage, @@ -437,7 +384,7 @@ pub struct MetricsCpu { wait: Percentage, } #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct MetricsDisk { percentage_used: Percentage, used: GigaBytes, @@ -445,7 +392,7 @@ pub struct MetricsDisk { capacity: GigaBytes, } #[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub struct Metrics { general: MetricsGeneral, memory: MetricsMemory, @@ -453,13 +400,8 @@ pub struct Metrics { disk: MetricsDisk, } -#[command(display(display_serializable))] -pub async fn metrics( - #[context] ctx: RpcContext, - #[allow(unused_variables)] - #[arg(long = "format")] - format: Option, -) -> Result { +// #[command(display(display_serializable))] +pub async fn metrics(ctx: RpcContext, _: Empty) -> Result { match ctx.metrics_cache.read().await.clone() { None => Err(Error { source: color_eyre::eyre::eyre!("No Metrics Found"), @@ -726,7 +668,7 @@ impl ProcStat { async fn get_proc_stat() -> Result { use tokio::io::AsyncBufReadExt; let mut cpu_line = String::new(); - let _n = tokio::io::BufReader::new(tokio::fs::File::open("/proc/stat").await?) + let _n = tokio::io::BufReader::new(open_file("/proc/stat").await?) .read_line(&mut cpu_line) .await?; let stats: Vec = cpu_line @@ -867,10 +809,10 @@ pub async fn get_mem_info() -> Result { #[instrument(skip_all)] async fn get_disk_info() -> Result { - let package_used_task = get_used("/embassy-data/package-data"); - let package_available_task = get_available("/embassy-data/package-data"); - let os_used_task = get_used("/embassy-data/main"); - let os_available_task = get_available("/embassy-data/main"); + let package_used_task = get_used(PACKAGE_DATA); + let package_available_task = get_available(PACKAGE_DATA); + let os_used_task = get_used(MAIN_DATA); + let os_available_task = get_available(MAIN_DATA); let (package_used, package_available, os_used, os_available) = futures::try_join!( package_used_task, @@ -892,6 +834,138 @@ async fn get_disk_info() -> Result { }) } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct SmtpValue { + #[arg(long)] + pub server: String, + #[arg(long)] + pub port: u16, + #[arg(long)] + pub from: String, + #[arg(long)] + pub login: String, + #[arg(long)] + pub password: Option, +} +pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> { + let smtp = Some(smtp); + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_smtp_mut() + .ser(&smtp) + }) + .await?; + if let Some(callbacks) = ctx.callbacks.get_system_smtp() { + callbacks.call(vector![to_value(&smtp)?]).await?; + } + Ok(()) +} +pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_smtp_mut() + .ser(&None) + }) + .await?; + if let Some(callbacks) = ctx.callbacks.get_system_smtp() { + callbacks.call(vector![Value::Null]).await?; + } + Ok(()) +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct TestSmtpParams { + #[arg(long)] + pub server: String, + #[arg(long)] + pub port: u16, + #[arg(long)] + pub from: String, + #[arg(long)] + pub to: String, + #[arg(long)] + pub login: String, + #[arg(long)] + pub password: Option, +} +pub async fn test_smtp( + _: RpcContext, + TestSmtpParams { + server, + port, + from, + to, + login, + password, + }: TestSmtpParams, +) -> Result<(), Error> { + use rustls_pki_types::pem::PemObject; + + let Some(pass_val) = password else { + return Err(Error::new( + eyre!("mail-send requires a password"), + ErrorKind::InvalidRequest, + )); + }; + + let mut root_cert_store = RootCertStore::empty(); + let pem = tokio::fs::read("/etc/ssl/certs/ca-certificates.crt").await?; + for cert in CertificateDer::pem_slice_iter(&pem) { + root_cert_store.add_parsable_certificates([cert.with_kind(ErrorKind::OpenSsl)?]); + } + + let cfg = Arc::new( + rustls::ClientConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions()? + .with_root_certificates(root_cert_store) + .with_no_client_auth(), + ); + let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg) + .implicit_tls(false) + .credentials((login.split("@").next().unwrap().to_owned(), pass_val)); + + fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> { + if addr.find("<").map_or(false, |start| { + addr.find(">").map_or(false, |end| start < end) + }) { + addr.split_once("<") + .map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr))) + .unwrap() + .into() + } else { + addr.into() + } + } + + let message = MessageBuilder::new() + .from(parse_address(&from)) + .to(parse_address(&to)) + .subject("StartOS Test Email") + .text_body("This is a test email sent from your StartOS Server"); + client + .connect() + .await + .map_err(|e| { + Error::new( + eyre!("mail-send connection error: {:?}", e), + ErrorKind::Unknown, + ) + })? + .send(message) + .await + .map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?; + Ok(()) +} + #[tokio::test] #[ignore] pub async fn test_get_temp() { diff --git a/core/startos/src/update/latest_information.rs b/core/startos/src/update/latest_information.rs deleted file mode 100644 index e897dd40a..000000000 --- a/core/startos/src/update/latest_information.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::collections::HashMap; - -use emver::Version; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; - -#[serde_as] -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct LatestInformation { - release_notes: HashMap, - headline: String, - #[serde_as(as = "DisplayFromStr")] - pub version: Version, -} - -/// Captured from https://beta-registry-0-3.start9labs.com/eos/latest 2021-09-24 -#[test] -fn latest_information_from_server() { - let data_from_server = r#"{"release-notes":{"0.3.0":"This major software release encapsulates the optimal performance, security, and management enhancments to the embassyOS experience."},"headline":"Major embassyOS release","version":"0.3.0"}"#; - let latest_information: LatestInformation = serde_json::from_str(data_from_server).unwrap(); - assert_eq!(latest_information.version.minor(), 3); -} diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 4ce57a8d1..7daa4f3b2 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -1,69 +1,221 @@ -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::collections::BTreeMap; +use std::path::Path; +use std::time::Duration; -use clap::ArgMatches; +use clap::{ArgAction, Parser}; use color_eyre::eyre::{eyre, Result}; -use emver::Version; -use helpers::{Rsync, RsyncOptions}; -use lazy_static::lazy_static; +use exver::{Version, VersionRange}; +use futures::TryStreamExt; +use helpers::{AtomicFile, NonDetachingJoinHandle}; +use imbl_value::json; +use itertools::Itertools; +use patch_db::json_ptr::JsonPointer; use reqwest::Url; -use rpc_toolkit::command; +use rpc_toolkit::HandlerArgs; +use serde::{Deserialize, Serialize}; use tokio::process::Command; -use tokio_stream::StreamExt; use tracing::instrument; +use ts_rs::TS; -use crate::context::RpcContext; -use crate::db::model::UpdateProgress; +use crate::context::{CliContext, RpcContext}; use crate::disk::mount::filesystem::bind::Bind; -use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::MountGuard; -use crate::notifications::NotificationLevel; +use crate::disk::mount::filesystem::block_dev::BlockDev; +use crate::disk::mount::filesystem::efivarfs::EfiVarFs; +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::disk::mount::filesystem::MountType; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; +use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::registry::marketplace::with_query_params; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::commitment::Commitment; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::sound::{ CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4, }; -use crate::update::latest_information::LatestInformation; +use crate::util::net::WebSocketExt; use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt, PLATFORM}; +use crate::PLATFORM; -mod latest_information; +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct UpdateSystemParams { + #[ts(type = "string")] + registry: Url, + #[ts(type = "string | null")] + #[arg(long = "to")] + target: Option, + #[arg(long = "no-progress", action = ArgAction::SetFalse)] + #[serde(default)] + progress: bool, +} -lazy_static! { - static ref UPDATED: AtomicBool = AtomicBool::new(false); +#[derive(Deserialize, Serialize, TS)] +pub struct UpdateSystemRes { + #[ts(type = "string | null")] + target: Option, + #[ts(type = "string | null")] + progress: Option, } /// An user/ daemon would call this to update the system to the latest version and do the updates available, /// and this will return something if there is an update, and in that case there will need to be a restart. -#[command( - rename = "update", - display(display_update_result), - metadata(sync_db = true) -)] #[instrument(skip_all)] pub async fn update_system( - #[context] ctx: RpcContext, - #[arg(rename = "marketplace-url")] marketplace_url: Url, -) -> Result { - if UPDATED.load(Ordering::SeqCst) { - return Ok(UpdateResult::NoUpdates); + ctx: RpcContext, + UpdateSystemParams { + target, + registry, + progress, + }: UpdateSystemParams, +) -> Result { + if ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_status_info() + .into_updated() + .de()? + { + return Err(Error::new(eyre!("Server was already updated. Please restart your device before attempting to update again."), ErrorKind::InvalidRequest)); } - Ok(if maybe_do_update(ctx, marketplace_url).await?.is_some() { - UpdateResult::Updating + let target = + maybe_do_update(ctx.clone(), registry, target.unwrap_or(VersionRange::Any)).await?; + let progress = if progress && target.is_some() { + let guid = Guid::new(); + ctx.clone() + .rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + if let Err(e) = async { + let mut sub = ctx + .db + .subscribe( + "/public/serverInfo/statusInfo/updateProgress" + .parse::() + .with_kind(ErrorKind::Database)?, + ) + .await; + loop { + let progress = ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_status_info() + .into_update_progress() + .de()?; + ws.send(axum::extract::ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.is_none() { + return ws.normal_close("complete").await; + } + tokio::select! { + _ = sub.recv() => (), + res = async { + loop { + if ws.recv().await.transpose().with_kind(ErrorKind::Network)?.is_none() { + return Ok(()) + } + } + } => { + return res + } + } + } + } + .await + { + tracing::error!("Error returning progress of update: {e}"); + tracing::debug!("{e:?}") + } + }, + Duration::from_secs(30), + ), + ) + .await; + Some(guid) } else { - UpdateResult::NoUpdates - }) + None + }; + Ok(UpdateSystemRes { target, progress }) +} + +pub async fn cli_update_system( + HandlerArgs { + context, + parent_method, + method, + raw_params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let res = from_value::( + context + .call_remote::( + &parent_method.into_iter().chain(method).join("."), + raw_params, + ) + .await?, + )?; + match res.target { + None => println!("No updates available"), + Some(v) => { + if let Some(progress) = res.progress { + let mut ws = context.ws_continuation(progress).await?; + let mut progress = PhasedProgressBar::new(&format!("Updating to v{v}...")); + let mut prev = None; + while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? { + if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg { + if let Some(snap) = + serde_json::from_str(&msg).with_kind(ErrorKind::Deserialization)? + { + progress.update(&snap); + prev = Some(snap); + } else { + break; + } + } + } + if let Some(mut prev) = prev { + for phase in &mut prev.phases { + phase.progress.complete(); + } + prev.overall.complete(); + progress.update(&prev); + } + } else { + println!("Updating to v{v}...") + } + } + } + Ok(()) } /// What is the status of the updates? #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] pub enum UpdateResult { NoUpdates, Updating, } -fn display_update_result(status: UpdateResult, _: &ArgMatches) { +pub fn display_update_result(_: UpdateSystemParams, status: UpdateResult) { match status { UpdateResult::Updating => { println!("Updating..."); @@ -75,34 +227,53 @@ fn display_update_result(status: UpdateResult, _: &ArgMatches) { } #[instrument(skip_all)] -async fn maybe_do_update(ctx: RpcContext, marketplace_url: Url) -> Result, Error> { +async fn maybe_do_update( + ctx: RpcContext, + registry: Url, + target: VersionRange, +) -> Result, Error> { let peeked = ctx.db.peek().await; - let latest_version: Version = ctx - .client - .get(with_query_params( - ctx.clone(), - format!("{}/eos/v0/latest", marketplace_url,).parse()?, - )) - .send() - .await - .with_kind(ErrorKind::Network)? - .json::() - .await - .with_kind(ErrorKind::Network)? - .version; - let current_version = peeked.as_server_info().as_version().de()?; - if latest_version < *current_version { + let current_version = peeked.as_public().as_server_info().as_version().de()?; + let mut available = from_value::>( + ctx.call_remote_with::( + "os.version.get", + json!({ + "source": current_version, + "target": target, + }), + RegistryUrlParams { registry }, + ) + .await?, + )?; + let Some((target_version, asset)) = available + .pop_last() + .and_then(|(v, mut info)| info.squashfs.remove(&**PLATFORM).map(|a| (v, a))) + else { return Ok(None); + }; + if !target_version.satisfies(&target) { + return Err(Error::new( + eyre!("got back version from registry that does not satisfy {target}"), + ErrorKind::Registry, + )); } - let eos_url = EosUrl { - base: marketplace_url, - version: latest_version, - }; + asset.validate(SIG_CONTEXT, asset.all_signers())?; + + let progress = FullProgressTracker::new(); + let prune_phase = progress.add_phase("Pruning Old OS Images".into(), Some(2)); + let mut download_phase = progress.add_phase("Downloading File".into(), Some(100)); + download_phase.set_total(asset.commitment.size); + let reverify_phase = progress.add_phase("Reverifying File".into(), Some(10)); + let sync_boot_phase = progress.add_phase("Syncing Boot Files".into(), Some(1)); + let finalize_phase = progress.add_phase("Finalizing Update".into(), Some(1)); + + let start_progress = progress.snapshot(); + let status = ctx .db .mutate(|db| { - let mut status = peeked.as_server_info().as_status_info().de()?; + let mut status = peeked.as_public().as_server_info().as_status_info().de()?; if status.update_progress.is_some() { return Err(Error::new( eyre!("Server is already updating!"), @@ -110,57 +281,84 @@ async fn maybe_do_update(ctx: RpcContext, marketplace_url: Url) -> Result { + actor: A, + shutdown: Option>, + waiting: Vec>, + recv: mpsc::UnboundedReceiver>, + handlers: Vec<( + Guid, + Arc>, + oneshot::Sender>, + BoxFuture<'static, Box>, + )>, + queue: BackgroundJobQueue, + #[pin] + bg_runner: BackgroundJobRunner, +} +impl Future for ConcurrentRunner { + type Output = (); + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let mut this = self.project(); + *this.shutdown = this.shutdown.take().and_then(|mut s| { + if s.poll_unpin(cx).is_pending() { + Some(s) + } else { + None + } + }); + if this.shutdown.is_some() { + while let std::task::Poll::Ready(Some((id, msg, reply))) = this.recv.poll_recv(cx) { + if this + .handlers + .iter() + .any(|(hid, f, _, _)| &id != hid && f(&*msg)) + { + this.waiting.push((id, msg, reply)); + } else { + let mut actor = this.actor.clone(); + let queue = this.queue.clone(); + this.handlers.push(( + id.clone(), + msg.conflicts_with(), + reply, + async move { msg.handle_with(id, &mut actor, &queue).await }.boxed(), + )) + } + } + } + // handlers + while { + let mut cont = false; + let complete = this + .handlers + .iter_mut() + .enumerate() + .filter_map(|(i, (_, _, _, f))| match f.poll_unpin(cx) { + std::task::Poll::Pending => None, + std::task::Poll::Ready(res) => Some((i, res)), + }) + .collect::>(); + for (idx, res) in complete.into_iter().rev() { + #[allow(clippy::let_underscore_future)] + let (_, f, reply, _) = this.handlers.swap_remove(idx); + let _ = reply.send(res); + // TODO: replace with Vec::extract_if once stable + if this.shutdown.is_some() { + let mut i = 0; + while i < this.waiting.len() { + if f(&*this.waiting[i].1) + && !this + .handlers + .iter() + .any(|(_, f, _, _)| f(&*this.waiting[i].1)) + { + let (id, msg, reply) = this.waiting.remove(i); + let mut actor = this.actor.clone(); + let queue = this.queue.clone(); + this.handlers.push(( + id.clone(), + msg.conflicts_with(), + reply, + async move { msg.handle_with(id, &mut actor, &queue).await } + .boxed(), + )); + cont = true; + } else { + i += 1; + } + } + } + } + cont + } {} + let _ = this.bg_runner.as_mut().poll(cx); + if this.waiting.is_empty() && this.handlers.is_empty() && this.recv.is_closed() { + std::task::Poll::Ready(()) + } else { + std::task::Poll::Pending + } + } +} + +pub struct ConcurrentActor { + shutdown: oneshot::Sender<()>, + runtime: NonDetachingJoinHandle<()>, + messenger: mpsc::UnboundedSender>, +} +impl ConcurrentActor { + pub fn new(mut actor: A) -> Self { + let (shutdown_send, shutdown_recv) = oneshot::channel(); + let (messenger_send, messenger_recv) = mpsc::unbounded_channel::>(); + let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move { + let (queue, runner) = BackgroundJobQueue::new(); + actor.init(&queue); + ConcurrentRunner { + actor, + shutdown: Some(shutdown_recv), + waiting: Vec::new(), + recv: messenger_recv, + handlers: Vec::new(), + queue, + bg_runner: runner, + } + .await + })); + Self { + shutdown: shutdown_send, + runtime, + messenger: messenger_send, + } + } + + /// Message is guaranteed to be queued immediately + pub fn queue( + &self, + id: Guid, + message: M, + ) -> impl Future> + where + A: Handler, + { + if self.runtime.is_finished() { + return futures::future::Either::Left(ready(Err(Error::new( + eyre!("actor runtime has exited"), + ErrorKind::Unknown, + )))); + } + let (reply_send, reply_recv) = oneshot::channel(); + self.messenger + .send((id, Box::new(message), reply_send)) + .unwrap(); + futures::future::Either::Right( + reply_recv + .map_err(|_| Error::new(eyre!("actor runtime has exited"), ErrorKind::Unknown)) + .and_then(|a| { + ready( + a.downcast() + .map_err(|_| { + Error::new( + eyre!("received incorrect type in response"), + ErrorKind::Incoherent, + ) + }) + .map(|a| *a), + ) + }), + ) + } + + pub async fn send(&self, id: Guid, message: M) -> Result + where + A: Handler, + { + self.queue(id, message).await + } + + pub async fn shutdown(self, strategy: PendingMessageStrategy) { + drop(self.messenger); + let timeout = match strategy { + PendingMessageStrategy::CancelAll => { + self.shutdown.send(()).unwrap(); + Some(Duration::from_secs(0)) + } + PendingMessageStrategy::FinishCurrentCancelPending { timeout } => { + self.shutdown.send(()).unwrap(); + timeout + } + PendingMessageStrategy::FinishAll { timeout } => timeout, + }; + let aborter = if let Some(timeout) = timeout { + let hdl = self.runtime.abort_handle(); + async move { + tokio::time::sleep(timeout).await; + hdl.abort(); + } + .boxed() + } else { + futures::future::pending().boxed() + }; + tokio::select! { + _ = aborter => (), + _ = self.runtime => (), + } + } +} diff --git a/core/startos/src/util/actor/mod.rs b/core/startos/src/util/actor/mod.rs new file mode 100644 index 000000000..d85e53757 --- /dev/null +++ b/core/startos/src/util/actor/mod.rs @@ -0,0 +1,156 @@ +use std::any::{Any, TypeId}; +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::Duration; + +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use tokio::sync::oneshot; + +#[allow(unused_imports)] +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::util::actor::background::BackgroundJobQueue; + +pub mod background; +pub mod concurrent; +pub mod simple; + +pub trait Actor: Sized + Send + 'static { + #[allow(unused_variables)] + fn init(&mut self, jobs: &BackgroundJobQueue) {} +} + +pub trait Handler: Actor { + type Response: Any + Send; + /// DRAGONS: this must be correctly implemented bi-directionally in order to work as expected + fn conflicts_with(#[allow(unused_variables)] msg: &M) -> ConflictBuilder { + ConflictBuilder::everything() + } + fn handle( + &mut self, + id: Guid, + msg: M, + jobs: &BackgroundJobQueue, + ) -> impl Future + Send; +} + +type ConflictFn = dyn Fn(&dyn Message) -> bool + Send + Sync; + +trait Message: Send + Any { + fn conflicts_with(&self) -> Arc>; + fn handle_with<'a>( + self: Box, + id: Guid, + actor: &'a mut A, + jobs: &'a BackgroundJobQueue, + ) -> BoxFuture<'a, Box>; +} +impl Message for M +where + A: Handler, +{ + fn conflicts_with(&self) -> Arc> { + A::conflicts_with(self).build() + } + fn handle_with<'a>( + self: Box, + id: Guid, + actor: &'a mut A, + jobs: &'a BackgroundJobQueue, + ) -> BoxFuture<'a, Box> { + async move { Box::new(actor.handle(id, *self, jobs).await) as Box }.boxed() + } +} +impl dyn Message { + #[inline] + pub fn is>(&self) -> bool { + let t = TypeId::of::(); + let concrete = self.type_id(); + t == concrete + } + #[inline] + pub unsafe fn downcast_ref_unchecked>(&self) -> &M { + debug_assert!(self.is::()); + unsafe { &*(self as *const dyn Message as *const M) } + } + #[inline] + fn downcast_ref>(&self) -> Option<&M> { + if self.is::() { + unsafe { Some(self.downcast_ref_unchecked()) } + } else { + None + } + } +} + +type Request = ( + Guid, + Box>, + oneshot::Sender>, +); + +pub enum PendingMessageStrategy { + CancelAll, + FinishCurrentCancelPending { timeout: Option }, + FinishAll { timeout: Option }, +} + +pub struct ConflictBuilder { + base: bool, + except: BTreeMap) -> bool + Send + Sync>>>, +} +impl ConflictBuilder { + pub const fn everything() -> Self { + Self { + base: true, + except: BTreeMap::new(), + } + } + pub const fn nothing() -> Self { + Self { + base: false, + except: BTreeMap::new(), + } + } + pub fn except(mut self) -> Self + where + A: Handler, + { + self.except.insert(TypeId::of::(), None); + self + } + pub fn except_if bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self + where + A: Handler, + { + self.except.insert( + TypeId::of::(), + Some(Box::new(move |m| { + if let Some(m) = m.downcast_ref() { + f(m) + } else { + false + } + })), + ); + self + } + fn build(self) -> Arc> { + Arc::new(move |m| { + self.base + ^ if let Some(entry) = self.except.get(&m.type_id()) { + if let Some(f) = entry { + f(m) + } else { + true + } + } else { + false + } + }) + } +} diff --git a/core/startos/src/util/actor/simple.rs b/core/startos/src/util/actor/simple.rs new file mode 100644 index 000000000..7f2ad388e --- /dev/null +++ b/core/startos/src/util/actor/simple.rs @@ -0,0 +1,119 @@ +use std::time::Duration; + +use futures::future::ready; +use futures::{Future, FutureExt, TryFutureExt}; +use helpers::NonDetachingJoinHandle; +use tokio::sync::oneshot::error::TryRecvError; +use tokio::sync::{mpsc, oneshot}; + +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{Actor, Handler, PendingMessageStrategy, Request}; + +pub struct SimpleActor { + shutdown: oneshot::Sender<()>, + runtime: NonDetachingJoinHandle<()>, + messenger: mpsc::UnboundedSender>, +} +impl SimpleActor { + pub fn new(mut actor: A) -> Self { + let (shutdown_send, mut shutdown_recv) = oneshot::channel(); + let (messenger_send, mut messenger_recv) = mpsc::unbounded_channel::>(); + let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move { + let (queue, mut runner) = BackgroundJobQueue::new(); + actor.init(&queue); + loop { + tokio::select! { + _ = &mut runner => (), + msg = messenger_recv.recv() => match msg { + Some((id, msg, reply)) if shutdown_recv.try_recv() == Err(TryRecvError::Empty) => { + tokio::select! { + res = msg.handle_with(id, &mut actor, &queue) => { let _ = reply.send(res); }, + _ = &mut runner => (), + } + } + _ => break, + }, + } + } + })); + Self { + shutdown: shutdown_send, + runtime, + messenger: messenger_send, + } + } + + /// Message is guaranteed to be queued immediately + pub fn queue( + &self, + message: M, + ) -> impl Future> + where + A: Handler, + { + if self.runtime.is_finished() { + return futures::future::Either::Left(ready(Err(Error::new( + eyre!("actor runtime has exited"), + ErrorKind::Unknown, + )))); + } + let (reply_send, reply_recv) = oneshot::channel(); + self.messenger + .send((Guid::new(), Box::new(message), reply_send)) + .unwrap(); + futures::future::Either::Right( + reply_recv + .map_err(|_| Error::new(eyre!("actor runtime has exited"), ErrorKind::Unknown)) + .and_then(|a| { + ready( + a.downcast() + .map_err(|_| { + Error::new( + eyre!("received incorrect type in response"), + ErrorKind::Incoherent, + ) + }) + .map(|a| *a), + ) + }), + ) + } + + pub async fn send(&self, message: M) -> Result + where + A: Handler, + { + self.queue(message).await + } + + pub async fn shutdown(self, strategy: PendingMessageStrategy) { + drop(self.messenger); + let timeout = match strategy { + PendingMessageStrategy::CancelAll => { + self.shutdown.send(()).unwrap(); + Some(Duration::from_secs(0)) + } + PendingMessageStrategy::FinishCurrentCancelPending { timeout } => { + self.shutdown.send(()).unwrap(); + timeout + } + PendingMessageStrategy::FinishAll { timeout } => timeout, + }; + let aborter = if let Some(timeout) = timeout { + let hdl = self.runtime.abort_handle(); + async move { + tokio::time::sleep(timeout).await; + hdl.abort(); + } + .boxed() + } else { + futures::future::pending().boxed() + }; + tokio::select! { + _ = aborter => (), + _ = self.runtime => (), + } + } +} diff --git a/core/startos/src/util/collections/eq_map.rs b/core/startos/src/util/collections/eq_map.rs new file mode 100644 index 000000000..5078866a5 --- /dev/null +++ b/core/startos/src/util/collections/eq_map.rs @@ -0,0 +1,1213 @@ +use std::borrow::Borrow; +use std::fmt; +use std::ops::{Index, IndexMut}; + +pub struct EqMap(Vec<(K, V)>); +impl Default for EqMap { + fn default() -> Self { + Self(Default::default()) + } +} +impl EqMap { + pub fn new() -> Self { + Self::default() + } + + pub fn clear(&mut self) { + self.0.clear() + } + + /// Returns the key-value pair corresponding to the supplied key as a borrowed tuple. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value_ref(&self, key: &Q) -> Option<&(K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0.iter().find(|(k, _)| k.borrow() == key) + } + + /// Returns the key-value pair corresponding to the supplied key as a mutably borrowed tuple. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value_mut(&mut self, key: &Q) -> Option<&mut (K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0.iter_mut().find(|(k, _)| k.borrow() == key) + } + + /// Returns a reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get(&1), Some(&"a")); + /// assert_eq!(map.get(&2), None); + /// ``` + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_ref(key).map(|(_, v)| v) + } + + /// Returns the key-value pair corresponding to the supplied key. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_ref(key).map(|(k, v)| (k, v)) + } + + /// Removes and returns an element in the map. + /// There is no guarantee about which element this might be + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// map.insert(2, "b"); + /// while let Some((_key, _val)) = map.pop() { } + /// assert!(map.is_empty()); + /// ``` + pub fn pop(&mut self) -> Option<(K, V)> + where + K: Eq, + { + self.0.pop() + } + + /// Returns `true` if the map contains a value for the specified key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.contains_key(&1), true); + /// assert_eq!(map.contains_key(&2), false); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow + Eq, + Q: Eq, + { + self.get(key).is_some() + } + + /// Returns a mutable reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// if let Some(x) = map.get_mut(&1) { + /// *x = "b"; + /// } + /// assert_eq!(map[&1], "b"); + /// ``` + // See `get` for implementation notes, this is basically a copy-paste with mut's added + pub fn get_mut(&mut self, key: &Q) -> Option<&mut V> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_mut(key).map(|(_, v)| v) + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not have this key present, `None` is returned. + /// + /// If the map did have this key present, the value is updated, and the old + /// value is returned. The key is not updated, though; this matters for + /// types that can be `==` without being identical. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// assert_eq!(map.insert(37, "a"), None); + /// assert_eq!(map.is_empty(), false); + /// + /// map.insert(37, "b"); + /// assert_eq!(map.insert(37, "c"), Some("b")); + /// assert_eq!(map[&37], "c"); + /// ``` + pub fn insert(&mut self, key: K, value: V) -> Option + where + K: Eq, + { + match self.entry(key) { + Occupied(mut entry) => Some(entry.insert(value)), + Vacant(entry) => { + entry.insert(value); + None + } + } + } + + /// Tries to insert a key-value pair into the map, and returns + /// a mutable reference to the value in the entry. + /// + /// If the map already had this key present, nothing is updated, and + /// an error containing the occupied entry and the value is returned. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// assert_eq!(map.try_insert(37, "a").unwrap(), &"a"); + /// + /// let err = map.try_insert(37, "b").unwrap_err(); + /// assert_eq!(err.entry.key(), &37); + /// assert_eq!(err.entry.get(), &"a"); + /// assert_eq!(err.value, "b"); + /// ``` + pub fn try_insert(&mut self, key: K, value: V) -> Result<&mut V, OccupiedError<'_, K, V>> + where + K: Eq, + { + match self.entry(key) { + Occupied(entry) => Err(OccupiedError { entry, value }), + Vacant(entry) => Ok(entry.insert(value)), + } + } + + /// Removes a key from the map, returning the value at the key if the key + /// was previously in the map. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.remove(&1), Some("a")); + /// assert_eq!(map.remove(&1), None); + /// ``` + pub fn remove(&mut self, key: &Q) -> Option + where + K: Borrow + Eq, + Q: Eq, + { + self.remove_entry(key).map(|(_, v)| v) + } + + /// Removes a key from the map, returning the stored key and value if the key + /// was previously in the map. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.remove_entry(&1), Some((1, "a"))); + /// assert_eq!(map.remove_entry(&1), None); + /// ``` + pub fn remove_entry(&mut self, key: &Q) -> Option<(K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0 + .iter() + .enumerate() + .find(|(_, (k, _))| k.borrow() == key) + .map(|(idx, _)| idx) + .map(|idx| self.0.swap_remove(idx)) + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)` returns `false`. + /// The elements are visited in ascending key order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap = (0..8).map(|x| (x, x*10)).collect(); + /// // Keep only the elements with even-numbered keys. + /// map.retain(|&k, _| k % 2 == 0); + /// assert!(map.into_iter().eq(vec![(0, 0), (2, 20), (4, 40), (6, 60)])); + /// ``` + #[inline] + pub fn retain(&mut self, mut f: F) + where + K: Eq, + F: FnMut(&K, &mut V) -> bool, + { + self.0.retain_mut(|(k, v)| f(k, v)) + } + + /// Moves all elements from `other` into `self`, leaving `other` empty. + /// + /// If a key from `other` is already present in `self`, the respective + /// value from `self` will be overwritten with the respective value from `other`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "a"); + /// a.insert(2, "b"); + /// a.insert(3, "c"); // Note: Key (3) also present in b. + /// + /// let mut b = EqMap::new(); + /// b.insert(3, "d"); // Note: Key (3) also present in a. + /// b.insert(4, "e"); + /// b.insert(5, "f"); + /// + /// a.append(&mut b); + /// + /// assert_eq!(a.len(), 5); + /// assert_eq!(b.len(), 0); + /// + /// assert_eq!(a[&1], "a"); + /// assert_eq!(a[&2], "b"); + /// assert_eq!(a[&3], "d"); // Note: "c" has been overwritten. + /// assert_eq!(a[&4], "e"); + /// assert_eq!(a[&5], "f"); + /// ``` + pub fn append(&mut self, other: &mut Self) + where + K: Eq, + { + for k in other.keys() { + self.remove(k); + } + self.0.append(&mut other.0) + } + + /// Gets the given key's corresponding entry in the map for in-place manipulation. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut count: EqMap<&str, usize> = EqMap::new(); + /// + /// // count the number of occurrences of letters in the vec + /// for x in ["a", "b", "a", "c", "a", "b"] { + /// count.entry(x).and_modify(|curr| *curr += 1).or_insert(1); + /// } + /// + /// assert_eq!(count["a"], 3); + /// assert_eq!(count["b"], 2); + /// assert_eq!(count["c"], 1); + /// ``` + pub fn entry(&mut self, key: K) -> Entry<'_, K, V> + where + K: Eq, + { + match self.0.iter().enumerate().find(|(_, (k, _))| k == &key) { + Some((idx, _)) => Occupied(OccupiedEntry { map: self, idx }), + None => Vacant(VacantEntry { key, map: self }), + } + } + + // /// Creates an iterator that visits all elements (key-value pairs) and + // /// uses a closure to determine if an element should be removed. If the + // /// closure returns `true`, the element is removed from the map and yielded. + // /// If the closure returns `false`, or panics, the element remains in the map + // /// and will not be yielded. + // /// + // /// The iterator also lets you mutate the value of each element in the + // /// closure, regardless of whether you choose to keep or remove it. + // /// + // /// If the returned `ExtractIf` is not exhausted, e.g. because it is dropped without iterating + // /// or the iteration short-circuits, then the remaining elements will be retained. + // /// Use [`retain`] with a negated predicate if you do not need the returned iterator. + // /// + // /// [`retain`]: EqMap::retain + // /// + // /// # Examples + // /// + // /// Splitting a map into even and odd keys, reusing the original map: + // /// + // /// ``` + // /// use startos::util::collections::EqMap; + // /// + // /// let mut map: EqMap = (0..8).map(|x| (x, x)).collect(); + // /// let evens: EqMap<_, _> = map.extract_if(|k, _v| k % 2 == 0).collect(); + // /// let odds = map; + // /// assert_eq!(evens.keys().copied().collect::>(), [0, 2, 4, 6]); + // /// assert_eq!(odds.keys().copied().collect::>(), [1, 3, 5, 7]); + // /// ``` + // pub fn extract_if(&mut self, pred: F) -> ExtractIf<'_, K, V, F> + // where + // K: Eq, + // F: FnMut(&K, &mut V) -> bool, + // { + // let (inner, alloc) = self.extract_if_inner(); + // ExtractIf { pred, inner, alloc } + // } + + /// Creates a consuming iterator visiting all the keys. + /// The map cannot be used after calling this. + /// The iterator element type is `K`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(2, "b"); + /// a.insert(1, "a"); + /// + /// let keys: Vec = a.into_keys().collect(); + /// assert_eq!(keys, [2, 1]); + /// ``` + #[inline] + pub fn into_keys(self) -> IntoKeys { + IntoKeys(self.0.into_iter()) + } + + /// Creates a consuming iterator visiting all the values. + /// The map cannot be used after calling this. + /// The iterator element type is `V`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "hello"); + /// a.insert(2, "goodbye"); + /// + /// let values: Vec<&str> = a.into_values().collect(); + /// assert_eq!(values, ["hello", "goodbye"]); + /// ``` + #[inline] + pub fn into_values(self) -> IntoValues { + IntoValues(self.0.into_iter()) + } + + pub fn iter_ref(&self) -> std::slice::Iter<'_, (K, V)> { + self.0.iter() + } + + /// Gets an iterator over the entries of the map, in no particular order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(3, "c"); + /// map.insert(2, "b"); + /// map.insert(1, "a"); + /// + /// for (key, value) in map.iter() { + /// println!("{key}: {value}"); + /// } + /// + /// let (first_key, first_value) = map.iter().next().unwrap(); + /// assert_eq!((*first_key, *first_value), (3, "c")); + /// ``` + pub fn iter(&self) -> std::iter::Map, fn(&(K, V)) -> (&K, &V)> { + self.0.iter().map(|(k, v)| (k, v)) + } + + /// Gets a mutable iterator over the entries of the map, in no particular order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::from([ + /// ("a", 1), + /// ("b", 2), + /// ("c", 3), + /// ]); + /// + /// // add 10 to the value if the key isn't "a" + /// for (key, value) in map.iter_mut() { + /// if key != &"a" { + /// *value += 10; + /// } + /// } + /// ``` + pub fn iter_mut( + &mut self, + ) -> std::iter::Map, fn(&mut (K, V)) -> (&K, &mut V)> { + self.0.iter_mut().map(|(k, v)| (&*k, v)) + } + + /// Gets an iterator over the keys of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(2, "b"); + /// a.insert(1, "a"); + /// + /// let keys: Vec<_> = a.keys().cloned().collect(); + /// assert_eq!(keys, [2, 1]); + /// ``` + pub fn keys(&self) -> std::iter::Map, fn(&(K, V)) -> &K> { + self.0.iter().map(|(k, _)| k) + } + + /// Gets an iterator over the values of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "hello"); + /// a.insert(2, "goodbye"); + /// + /// let values: Vec<&str> = a.values().cloned().collect(); + /// assert_eq!(values, ["hello", "goodbye"]); + /// ``` + pub fn values(&self) -> std::iter::Map, fn(&(K, V)) -> &V> { + self.0.iter().map(|(_, v)| v) + } + + /// Gets a mutable iterator over the values of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, String::from("hello")); + /// a.insert(2, String::from("goodbye")); + /// + /// for value in a.values_mut() { + /// value.push_str("!"); + /// } + /// + /// let values: Vec = a.values().cloned().collect(); + /// assert_eq!(values, [String::from("hello!"), + /// String::from("goodbye!")]); + /// ``` + pub fn values_mut( + &mut self, + ) -> std::iter::Map, fn(&mut (K, V)) -> &mut V> { + self.0.iter_mut().map(|(_, v)| v) + } + + /// Returns the number of elements in the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// assert_eq!(a.len(), 0); + /// a.insert(1, "a"); + /// assert_eq!(a.len(), 1); + /// ``` + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the map contains no elements. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// assert!(a.is_empty()); + /// a.insert(1, "a"); + /// assert!(!a.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl fmt::Debug for EqMap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +impl Index<&Q> for EqMap +where + K: Borrow + Eq, + Q: Eq, +{ + type Output = V; + + /// Returns a reference to the value corresponding to the supplied key. + /// + /// # Panics + /// + /// Panics if the key is not present in the `BTreeMap`. + #[inline] + fn index(&self, key: &Q) -> &V { + self.get(key).expect("no entry found for key") + } +} + +impl IndexMut<&Q> for EqMap +where + K: Borrow + Eq, + Q: Eq, +{ + /// Returns a reference to the value corresponding to the supplied key. + /// + /// # Panics + /// + /// Panics if the key is not present in the `BTreeMap`. + #[inline] + fn index_mut(&mut self, key: &Q) -> &mut V { + self.get_mut(key).expect("no entry found for key") + } +} + +impl IntoIterator for EqMap { + type IntoIter = std::vec::IntoIter<(K, V)>; + type Item = (K, V); + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Extend<(K, V)> for EqMap { + fn extend>(&mut self, iter: T) { + self.0.extend(iter) + } +} + +impl FromIterator<(K, V)> for EqMap { + fn from_iter>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl From<[(K, V); N]> for EqMap { + /// Converts a `[(K, V); N]` into a `EqMap<(K, V)>`. + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let map1 = EqMap::from([(1, 2), (3, 4)]); + /// let map2: EqMap<_, _> = [(1, 2), (3, 4)].into(); + /// assert_eq!(map1, map2); + /// ``` + fn from(arr: [(K, V); N]) -> Self { + EqMap(Vec::from(arr)) + } +} + +impl PartialEq for EqMap { + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() && self.iter().all(|(k, v)| other.get(k) == Some(v)) + } +} +impl Eq for EqMap {} + +use Entry::*; + +/// A view into a single entry in a map, which may either be vacant or occupied. +/// +/// This `enum` is constructed from the [`entry`] method on [`EqMap`]. +/// +/// [`entry`]: EqMap::entry +pub enum Entry<'a, K: Eq + 'a, V: 'a> { + Vacant(VacantEntry<'a, K, V>), + + /// An occupied entry. + Occupied(OccupiedEntry<'a, K, V>), +} + +impl fmt::Debug for Entry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Vacant(ref v) => f.debug_tuple("Entry").field(v).finish(), + Occupied(ref o) => f.debug_tuple("Entry").field(o).finish(), + } + } +} + +/// A view into a vacant entry in a `EqMap`. +/// It is part of the [`Entry`] enum. +pub struct VacantEntry<'a, K: Eq, V> { + key: K, + map: &'a mut EqMap, +} + +impl fmt::Debug for VacantEntry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VacantEntry").field(self.key()).finish() + } +} + +/// A view into an occupied entry in a `EqMap`. +/// It is part of the [`Entry`] enum. +pub struct OccupiedEntry<'a, K: Eq, V> { + map: &'a mut EqMap, + idx: usize, +} + +impl fmt::Debug for OccupiedEntry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OccupiedEntry") + .field("key", self.key()) + .field("value", self.get()) + .finish() + } +} + +/// The error returned by [`try_insert`](EqMap::try_insert) when the key already exists. +/// +/// Contains the occupied entry, and the value that was not inserted. +pub struct OccupiedError<'a, K: Eq + 'a, V: 'a> { + /// The entry in the map that was already occupied. + pub entry: OccupiedEntry<'a, K, V>, + /// The value which was not inserted, because the entry was already occupied. + pub value: V, +} + +impl fmt::Debug for OccupiedError<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OccupiedError") + .field("key", self.entry.key()) + .field("old_value", self.entry.get()) + .field("new_value", &self.value) + .finish() + } +} + +impl<'a, K: fmt::Debug + Eq, V: fmt::Debug> fmt::Display for OccupiedError<'a, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "failed to insert {:?}, key {:?} already exists with value {:?}", + self.value, + self.entry.key(), + self.entry.get(), + ) + } +} + +impl<'a, K: fmt::Debug + Eq, V: fmt::Debug> std::error::Error for OccupiedError<'a, K, V> { + fn description(&self) -> &str { + "key already exists" + } +} + +impl<'a, K: Eq, V> Entry<'a, K, V> { + /// Ensures a value is in the entry by inserting the default if empty, and returns + /// a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// ``` + pub fn or_insert(self, default: V) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(default), + } + } + + /// Ensures a value is in the entry by inserting the result of the default function if empty, + /// and returns a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, String> = EqMap::new(); + /// let s = "hoho".to_string(); + /// + /// map.entry("poneyland").or_insert_with(|| s); + /// + /// assert_eq!(map["poneyland"], "hoho".to_string()); + /// ``` + pub fn or_insert_with V>(self, default: F) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(default()), + } + } + + /// Ensures a value is in the entry by inserting, if empty, the result of the default function. + /// This method allows for generating key-derived values for insertion by providing the default + /// function a reference to the key that was moved during the `.entry(key)` method call. + /// + /// The reference to the moved key is provided so that cloning or copying the key is + /// unnecessary, unlike with `.or_insert_with(|| ... )`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// map.entry("poneyland").or_insert_with_key(|key| key.chars().count()); + /// + /// assert_eq!(map["poneyland"], 9); + /// ``` + #[inline] + pub fn or_insert_with_key V>(self, default: F) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => { + let value = default(entry.key()); + entry.insert(value) + } + } + } + + /// Returns a reference to this entry's key. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + pub fn key(&self) -> &K { + match *self { + Occupied(ref entry) => entry.key(), + Vacant(ref entry) => entry.key(), + } + } + + /// Provides in-place mutable access to an occupied entry before any + /// potential inserts into the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// map.entry("poneyland") + /// .and_modify(|e| { *e += 1 }) + /// .or_insert(42); + /// assert_eq!(map["poneyland"], 42); + /// + /// map.entry("poneyland") + /// .and_modify(|e| { *e += 1 }) + /// .or_insert(42); + /// assert_eq!(map["poneyland"], 43); + /// ``` + pub fn and_modify(self, f: F) -> Self + where + F: FnOnce(&mut V), + { + match self { + Occupied(mut entry) => { + f(entry.get_mut()); + Occupied(entry) + } + Vacant(entry) => Vacant(entry), + } + } +} + +impl<'a, K: Eq, V: Default> Entry<'a, K, V> { + /// Ensures a value is in the entry by inserting the default value if empty, + /// and returns a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, Option> = EqMap::new(); + /// map.entry("poneyland").or_default(); + /// + /// assert_eq!(map["poneyland"], None); + /// ``` + pub fn or_default(self) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(Default::default()), + } + } +} + +impl<'a, K: Eq, V> VacantEntry<'a, K, V> { + /// Gets a reference to the key that would be used when inserting a value + /// through the VacantEntry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + pub fn key(&self) -> &K { + &self.key + } + + /// Take ownership of the key. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// if let Entry::Vacant(v) = map.entry("poneyland") { + /// v.into_key(); + /// } + /// ``` + pub fn into_key(self) -> K { + self.key + } + + /// Sets the value of the entry with the `VacantEntry`'s key, + /// and returns a mutable reference to it. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, u32> = EqMap::new(); + /// + /// if let Entry::Vacant(o) = map.entry("poneyland") { + /// o.insert(37); + /// } + /// assert_eq!(map["poneyland"], 37); + /// ``` + pub fn insert(self, value: V) -> &'a mut V { + self.map.0.push((self.key, value)); + self.map.0.last_mut().map(|(_, v)| v).unwrap() + } +} + +impl<'a, K: Eq, V> OccupiedEntry<'a, K, V> { + /// Gets a reference to the key in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + #[must_use] + pub fn key(&self) -> &K { + &self.map.0[self.idx].0 + } + + /// Take ownership of the key and value from the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// // We delete the entry from the map. + /// o.remove_entry(); + /// } + /// + /// // If now try to get the value, it will panic: + /// // println!("{}", map["poneyland"]); + /// ``` + pub fn remove_entry(self) -> (K, V) { + self.map.0.swap_remove(self.idx) + } + + /// Gets a reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// assert_eq!(o.get(), &12); + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &V { + &self.map.0[self.idx].1 + } + + /// Gets a mutable reference to the value in the entry. + /// + /// If you need a reference to the `OccupiedEntry` that may outlive the + /// destruction of the `Entry` value, see [`into_mut`]. + /// + /// [`into_mut`]: OccupiedEntry::into_mut + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// if let Entry::Occupied(mut o) = map.entry("poneyland") { + /// *o.get_mut() += 10; + /// assert_eq!(*o.get(), 22); + /// + /// // We can use the same Entry multiple times. + /// *o.get_mut() += 2; + /// } + /// assert_eq!(map["poneyland"], 24); + /// ``` + pub fn get_mut(&mut self) -> &mut V { + &mut self.map.0[self.idx].1 + } + + /// Converts the entry into a mutable reference to its value. + /// + /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// + /// [`get_mut`]: OccupiedEntry::get_mut + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// *o.into_mut() += 10; + /// } + /// assert_eq!(map["poneyland"], 22); + /// ``` + #[must_use = "`self` will be dropped if the result is not used"] + pub fn into_mut(self) -> &'a mut V { + &mut self.map.0[self.idx].1 + } + + /// Sets the value of the entry with the `OccupiedEntry`'s key, + /// and returns the entry's old value. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(mut o) = map.entry("poneyland") { + /// assert_eq!(o.insert(15), 12); + /// } + /// assert_eq!(map["poneyland"], 15); + /// ``` + pub fn insert(&mut self, value: V) -> V { + std::mem::replace(self.get_mut(), value) + } + + /// Takes the value of the entry out of the map, and returns it. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// assert_eq!(o.remove(), 12); + /// } + /// // If we try to get "poneyland"'s value, it'll panic: + /// // println!("{}", map["poneyland"]); + /// ``` + pub fn remove(self) -> V { + self.remove_entry().1 + } +} + +pub struct IntoValues(std::vec::IntoIter<(K, V)>); +impl<'a, K: Eq, V> From> for std::vec::IntoIter<(K, V)> { + fn from(value: IntoValues) -> Self { + value.0 + } +} +impl Iterator for IntoValues { + type Item = V; + fn next(&mut self) -> Option { + self.0.next().map(|(_, v)| v) + } + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + fn count(self) -> usize + where + Self: Sized, + { + self.0.count() + } +} +impl DoubleEndedIterator for IntoValues { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, v)| v) + } +} +impl ExactSizeIterator for IntoValues { + fn len(&self) -> usize { + self.0.len() + } +} + +pub struct IntoKeys(std::vec::IntoIter<(K, V)>); +impl<'a, K: Eq, V> From> for std::vec::IntoIter<(K, V)> { + fn from(value: IntoKeys) -> Self { + value.0 + } +} +impl Iterator for IntoKeys { + type Item = K; + fn next(&mut self) -> Option { + self.0.next().map(|(k, _)| k) + } + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + fn count(self) -> usize + where + Self: Sized, + { + self.0.count() + } +} +impl DoubleEndedIterator for IntoKeys { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(k, _)| k) + } +} +impl ExactSizeIterator for IntoKeys { + fn len(&self) -> usize { + self.0.len() + } +} diff --git a/core/startos/src/util/collections/mod.rs b/core/startos/src/util/collections/mod.rs new file mode 100644 index 000000000..aa6e3ddb5 --- /dev/null +++ b/core/startos/src/util/collections/mod.rs @@ -0,0 +1,3 @@ +pub mod eq_map; + +pub use eq_map::EqMap; diff --git a/core/startos/src/util/config.rs b/core/startos/src/util/config.rs deleted file mode 100644 index f719f563f..000000000 --- a/core/startos/src/util/config.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::fs::File; -use std::path::{Path, PathBuf}; - -use patch_db::Value; -use serde::Deserialize; - -use crate::prelude::*; -use crate::util::serde::IoFormat; -use crate::{Config, Error}; - -pub const DEVICE_CONFIG_PATH: &str = "/media/embassy/config/config.yaml"; -pub const CONFIG_PATH: &str = "/etc/embassy/config.yaml"; -pub const CONFIG_PATH_LOCAL: &str = ".embassy/config.yaml"; - -pub fn local_config_path() -> Option { - if let Ok(home) = std::env::var("HOME") { - Some(Path::new(&home).join(CONFIG_PATH_LOCAL)) - } else { - None - } -} - -/// BLOCKING -pub fn load_config_from_paths<'a, T: for<'de> Deserialize<'de>>( - paths: impl IntoIterator>, -) -> Result { - let mut config = Default::default(); - for path in paths { - if path.as_ref().exists() { - let format: IoFormat = path - .as_ref() - .extension() - .and_then(|s| s.to_str()) - .map(|f| f.parse()) - .transpose()? - .unwrap_or_default(); - let new = format.from_reader(File::open(path)?)?; - config = merge_configs(config, new); - } - } - from_value(Value::Object(config)) -} - -pub fn merge_configs(mut first: Config, second: Config) -> Config { - for (k, v) in second.into_iter() { - let new = match first.remove(&k) { - None => v, - Some(old) => match (old, v) { - (Value::Object(first), Value::Object(second)) => { - Value::Object(merge_configs(first, second)) - } - (first, _) => first, - }, - }; - first.insert(k, new); - } - first -} diff --git a/core/startos/src/util/cpupower.rs b/core/startos/src/util/cpupower.rs index cc4ac5ef4..db625f90e 100644 --- a/core/startos/src/util/cpupower.rs +++ b/core/startos/src/util/cpupower.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use imbl::OrdMap; use tokio::process::Command; +use ts_rs::TS; use crate::prelude::*; use crate::util::Invoke; @@ -13,7 +14,10 @@ pub const GOVERNOR_HEIRARCHY: &[Governor] = &[ Governor(Cow::Borrowed("conservative")), ]; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, TS, +)] +#[ts(export, type = "string")] pub struct Governor(Cow<'static, str>); impl std::str::FromStr for Governor { type Err = std::convert::Infallible; diff --git a/core/startos/src/util/crypto.rs b/core/startos/src/util/crypto.rs index 5c1aed01e..bb164b6ad 100644 --- a/core/startos/src/util/crypto.rs +++ b/core/startos/src/util/crypto.rs @@ -7,3 +7,124 @@ pub fn ed25519_expand_key(key: &SecretKey) -> [u8; EXPANDED_SECRET_KEY_LENGTH] { ) .to_bytes() } + +use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher}; +use aes::Aes256Ctr; +use hmac::Hmac; +use josekit::jwk::Jwk; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use tracing::instrument; +use ts_rs::TS; + +use crate::prelude::*; + +pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey { + let mut aeskey = CipherKey::::default(); + pbkdf2::pbkdf2::>( + password.as_ref(), + salt.as_ref(), + 1000, + aeskey.as_mut_slice(), + ) + .unwrap(); + aeskey +} + +pub fn encrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec { + let prefix: [u8; 32] = rand::random(); + let aeskey = pbkdf2(password.as_ref(), &prefix[16..]); + let ctr = Nonce::::from_slice(&prefix[..16]); + let mut aes = Aes256Ctr::new(&aeskey, ctr); + let mut res = Vec::with_capacity(32 + input.as_ref().len()); + res.extend_from_slice(&prefix[..]); + res.extend_from_slice(input.as_ref()); + aes.apply_keystream(&mut res[32..]); + res +} + +pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec { + if input.as_ref().len() < 32 { + return Vec::new(); + } + let (prefix, rest) = input.as_ref().split_at(32); + let aeskey = pbkdf2(password.as_ref(), &prefix[16..]); + let ctr = Nonce::::from_slice(&prefix[..16]); + let mut aes = Aes256Ctr::new(&aeskey, ctr); + let mut res = rest.to_vec(); + aes.apply_keystream(&mut res); + res +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct EncryptedWire { + #[ts(type = "any")] + encrypted: Value, +} +impl EncryptedWire { + #[instrument(skip_all)] + pub fn decrypt(self, current_secret: impl AsRef) -> Option { + let current_secret = current_secret.as_ref(); + + let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs + .decrypter_from_jwk(current_secret) + { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not setup awk"); + tracing::debug!("{:?}", e); + return None; + } + }; + let encrypted = match serde_json::to_string(&self.encrypted) { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not deserialize"); + tracing::debug!("{:?}", e); + + return None; + } + }; + let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) { + Ok(a) => a, + Err(e) => { + tracing::warn!("Could not decrypt"); + tracing::debug!("{:?}", e); + return None; + } + }; + match String::from_utf8(decoded) { + Ok(a) => Some(a), + Err(e) => { + tracing::warn!("Could not decrypt into utf8"); + tracing::debug!("{:?}", e); + return None; + } + } + } +} + +/// We created this test by first making the private key, then restoring from this private key for recreatability. +/// After this the frontend then encoded an password, then we are testing that the output that we got (hand coded) +/// will be the shape we want. +#[test] +fn test_gen_awk() { + let private_key: Jwk = serde_json::from_str( + r#"{ + "kty": "EC", + "crv": "P-256", + "d": "3P-MxbUJtEhdGGpBCRFXkUneGgdyz_DGZWfIAGSCHOU", + "x": "yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4", + "y": "8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI" + }"#, + ) + .unwrap(); + let encrypted: EncryptedWire = serde_json::from_str(r#"{ + "encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" } + }"#).unwrap(); + assert_eq!( + "testing12345", + &encrypted.decrypt(std::sync::Arc::new(private_key)).unwrap() + ); +} diff --git a/core/startos/src/util/docker.rs b/core/startos/src/util/docker.rs deleted file mode 100644 index fb6bc15f4..000000000 --- a/core/startos/src/util/docker.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::net::Ipv4Addr; -use std::time::Duration; - -use models::{Error, ErrorKind, PackageId, ResultExt, Version}; -use nix::sys::signal::Signal; -use tokio::process::Command; - -use crate::util::Invoke; - -#[cfg(feature = "docker")] -pub const CONTAINER_TOOL: &str = "docker"; -#[cfg(not(feature = "docker"))] -pub const CONTAINER_TOOL: &str = "podman"; - -#[cfg(feature = "docker")] -pub const CONTAINER_DATADIR: &str = "/var/lib/docker"; -#[cfg(not(feature = "docker"))] -pub const CONTAINER_DATADIR: &str = "/var/lib/containers"; - -pub struct DockerImageSha(String); - -// docker images start9/${package}/*:${version} -q --no-trunc -pub async fn images_for( - package: &PackageId, - version: &Version, -) -> Result, Error> { - Ok(String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("images") - .arg(format!("start9/{package}/*:{version}")) - .arg("--no-trunc") - .arg("-q") - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .map(|l| DockerImageSha(l.trim().to_owned())) - .collect()) -} - -// docker rmi -f ${sha} -pub async fn remove_image(sha: &DockerImageSha) -> Result<(), Error> { - match Command::new(CONTAINER_TOOL) - .arg("rmi") - .arg("-f") - .arg(&sha.0) - .invoke(ErrorKind::Docker) - .await - .map(|_| ()) - { - Err(e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such image") => - { - Ok(()) - } - a => a, - }?; - Ok(()) -} - -// docker image prune -f -pub async fn prune_images() -> Result<(), Error> { - Command::new(CONTAINER_TOOL) - .arg("image") - .arg("prune") - .arg("-f") - .invoke(ErrorKind::Docker) - .await?; - Ok(()) -} - -// docker container inspect ${name} --format '{{.NetworkSettings.Networks.start9.IPAddress}}' -pub async fn get_container_ip(name: &str) -> Result, Error> { - match Command::new(CONTAINER_TOOL) - .arg("container") - .arg("inspect") - .arg(name) - .arg("--format") - .arg("{{.NetworkSettings.Networks.start9.IPAddress}}") - .invoke(ErrorKind::Docker) - .await - { - Err(e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - Ok(None) - } - Err(e) => Err(e), - Ok(a) => { - let out = std::str::from_utf8(&a)?.trim(); - if out.is_empty() { - Ok(None) - } else { - Ok(Some({ - out.parse() - .with_ctx(|_| (ErrorKind::ParseNetAddress, out.to_string()))? - })) - } - } - } -} - -// docker stop -t ${timeout} -s ${signal} ${name} -pub async fn stop_container( - name: &str, - timeout: Option, - signal: Option, -) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("stop"); - if let Some(dur) = timeout { - cmd.arg("-t").arg(dur.as_secs().to_string()); - } - if let Some(sig) = signal { - cmd.arg("-s").arg(sig.to_string()); - } - cmd.arg(name); - match cmd.invoke(ErrorKind::Docker).await { - Ok(_) => Ok(()), - Err(mut e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - e.kind = ErrorKind::NotFound; - Err(e) - } - Err(e) => Err(e), - } -} - -// docker kill -s ${signal} ${name} -pub async fn kill_container(name: &str, signal: Option) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("kill"); - if let Some(sig) = signal { - cmd.arg("-s").arg(sig.to_string()); - } - cmd.arg(name); - match cmd.invoke(ErrorKind::Docker).await { - Ok(_) => Ok(()), - Err(mut e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - e.kind = ErrorKind::NotFound; - Err(e) - } - Err(e) => Err(e), - } -} - -// docker pause ${name} -pub async fn pause_container(name: &str) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("pause"); - cmd.arg(name); - match cmd.invoke(ErrorKind::Docker).await { - Ok(_) => Ok(()), - Err(mut e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - e.kind = ErrorKind::NotFound; - Err(e) - } - Err(e) => Err(e), - } -} - -// docker unpause ${name} -pub async fn unpause_container(name: &str) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("unpause"); - cmd.arg(name); - match cmd.invoke(ErrorKind::Docker).await { - Ok(_) => Ok(()), - Err(mut e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - e.kind = ErrorKind::NotFound; - Err(e) - } - Err(e) => Err(e), - } -} - -// docker rm -f ${name} -pub async fn remove_container(name: &str, force: bool) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("rm"); - if force { - cmd.arg("-f"); - } - cmd.arg(name); - match cmd.invoke(ErrorKind::Docker).await { - Ok(_) => Ok(()), - Err(e) - if e.source - .to_string() - .to_ascii_lowercase() - .contains("no such container") => - { - Ok(()) - } - Err(e) => Err(e), - } -} - -// docker network create -d bridge --subnet ${subnet} --opt com.podman.network.bridge.name=${bridge_name} -pub async fn create_bridge_network( - name: &str, - subnet: &str, - bridge_name: &str, -) -> Result<(), Error> { - let mut cmd = Command::new(CONTAINER_TOOL); - cmd.arg("network").arg("create"); - cmd.arg("-d").arg("bridge"); - cmd.arg("--subnet").arg(subnet); - cmd.arg("--opt") - .arg(format!("com.docker.network.bridge.name={bridge_name}")); - cmd.arg(name); - cmd.invoke(ErrorKind::Docker).await?; - Ok(()) -} diff --git a/core/startos/src/util/future.rs b/core/startos/src/util/future.rs new file mode 100644 index 000000000..c690f9754 --- /dev/null +++ b/core/startos/src/util/future.rs @@ -0,0 +1,175 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::future::{abortable, pending, BoxFuture, FusedFuture}; +use futures::stream::{AbortHandle, Abortable, BoxStream}; +use futures::{Future, FutureExt, Stream, StreamExt}; +use tokio::sync::watch; + +use crate::prelude::*; + +#[pin_project::pin_project(PinnedDrop)] +pub struct DropSignaling { + #[pin] + fut: F, + on_drop: watch::Sender, +} +impl DropSignaling { + pub fn new(fut: F) -> Self { + Self { + fut, + on_drop: watch::channel(false).0, + } + } + pub fn subscribe(&self) -> DropHandle { + DropHandle(self.on_drop.subscribe()) + } +} +impl Future for DropSignaling +where + F: Future, +{ + type Output = F::Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.fut.poll(cx) + } +} +#[pin_project::pinned_drop] +impl PinnedDrop for DropSignaling { + fn drop(self: Pin<&mut Self>) { + let _ = self.on_drop.send(true); + } +} + +#[derive(Clone)] +pub struct DropHandle(watch::Receiver); +impl DropHandle { + pub async fn wait(&mut self) { + let _ = self.0.wait_for(|a| *a).await; + } +} + +#[pin_project::pin_project] +pub struct RemoteCancellable { + #[pin] + fut: Abortable>, + on_drop: DropHandle, + handle: AbortHandle, +} +impl RemoteCancellable { + pub fn new(fut: F) -> Self { + let sig_fut = DropSignaling::new(fut); + let on_drop = sig_fut.subscribe(); + let (fut, handle) = abortable(sig_fut); + Self { + fut, + on_drop, + handle, + } + } +} +impl RemoteCancellable { + pub fn cancellation_handle(&self) -> CancellationHandle { + CancellationHandle { + on_drop: self.on_drop.clone(), + handle: self.handle.clone(), + } + } +} +impl Future for RemoteCancellable +where + F: Future, +{ + type Output = Option; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.fut.poll(cx).map(|a| a.ok()) + } +} + +#[derive(Clone)] +pub struct CancellationHandle { + on_drop: DropHandle, + handle: AbortHandle, +} +impl CancellationHandle { + pub fn cancel(&mut self) { + self.handle.abort(); + } + + pub async fn cancel_and_wait(&mut self) { + self.handle.abort(); + self.on_drop.wait().await + } +} + +#[derive(Default)] +pub struct Until<'a> { + streams: Vec>>, + async_fns: Vec BoxFuture<'a, Result<(), Error>> + Send + 'a>>, +} +impl<'a> Until<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn with_stream( + mut self, + stream: impl Stream> + Send + 'a, + ) -> Self { + self.streams.push(stream.boxed()); + self + } + + pub fn with_async_fn(mut self, mut f: F) -> Self + where + F: FnMut() -> Fut + Send + 'a, + Fut: Future> + FusedFuture + Send + 'a, + { + self.async_fns.push(Box::new(move || f().boxed())); + self + } + + pub async fn run> + Send>( + &mut self, + fut: Fut, + ) -> Result<(), Error> { + let (res, _, _) = futures::future::select_all( + self.streams + .iter_mut() + .map(|s| { + async { + s.next().await.transpose()?.ok_or_else(|| { + Error::new(eyre!("stream is empty"), ErrorKind::Cancelled) + }) + } + .boxed() + }) + .chain(self.async_fns.iter_mut().map(|f| f())) + .chain([async { + fut.await?; + pending().await + } + .boxed()]), + ) + .await; + res + } +} + +#[tokio::test] +async fn test_cancellable() { + use std::sync::Arc; + + let arc = Arc::new(()); + let weak = Arc::downgrade(&arc); + let cancellable = RemoteCancellable::new(async move { + futures::future::pending::<()>().await; + drop(arc) + }); + let mut handle = cancellable.cancellation_handle(); + tokio::spawn(cancellable); + handle.cancel_and_wait().await; + assert!(weak.strong_count() == 0); +} diff --git a/core/startos/src/util/http_reader.rs b/core/startos/src/util/http_reader.rs index 87e8c114e..02a9f57ae 100644 --- a/core/startos/src/util/http_reader.rs +++ b/core/startos/src/util/http_reader.rs @@ -6,11 +6,11 @@ use std::io::Error as StdIOError; use std::pin::Pin; use std::task::{Context, Poll}; +use bytes::Bytes; use color_eyre::eyre::eyre; use futures::Stream; -use http::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; -use hyper::body::Bytes; use pin_project::pin_project; +use reqwest::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; use reqwest::{Client, Url}; use tokio::io::{AsyncRead, AsyncSeek}; @@ -359,22 +359,3 @@ async fn main_test() { assert_eq!(buf.len(), test_reader.total_bytes) } - -#[tokio::test] -#[ignore] -async fn s9pk_test() { - use tokio::io::BufReader; - - let http_url = Url::parse("http://qhc6ac47cytstejcepk2ia3ipadzjhlkc5qsktsbl4e7u2krfmfuaqqd.onion/content/files/2022/09/ghost.s9pk").unwrap(); - - println!("Getting this resource: {}", http_url); - let test_reader = - BufReader::with_capacity(1024 * 1024, HttpReader::new(http_url).await.unwrap()); - - let mut s9pk = crate::s9pk::reader::S9pkReader::from_reader(test_reader, false) - .await - .unwrap(); - - let manifest = s9pk.manifest().await.unwrap(); - assert_eq!(&manifest.id.to_string(), "ghost"); -} diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 282a2db8e..f0bae7a0a 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -1,22 +1,30 @@ +use std::collections::VecDeque; use std::future::Future; use std::io::Cursor; +use std::mem::MaybeUninit; use std::os::unix::prelude::MetadataExt; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::atomic::AtomicU64; -use std::task::Poll; +use std::sync::Arc; +use std::task::{Poll, Waker}; use std::time::Duration; +use bytes::{Buf, BytesMut}; use futures::future::{BoxFuture, Fuse}; -use futures::{AsyncSeek, FutureExt, TryStreamExt}; +use futures::{AsyncSeek, FutureExt, Stream, TryStreamExt}; use helpers::NonDetachingJoinHandle; use nix::unistd::{Gid, Uid}; +use tokio::fs::{File, OpenOptions}; use tokio::io::{ duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf, }; use tokio::net::TcpStream; +use tokio::sync::{Notify, OwnedMutexGuard}; use tokio::time::{Instant, Sleep}; -use crate::ResultExt; +use crate::prelude::*; +use crate::{CAP_1_KiB, CAP_1_MiB}; pub trait AsyncReadSeek: AsyncRead + AsyncSeek {} impl AsyncReadSeek for T {} @@ -268,6 +276,81 @@ pub fn response_to_reader(response: reqwest::Response) -> impl AsyncRead + Unpin })) } +#[pin_project::pin_project] +pub struct IOHook<'a, T> { + #[pin] + pub io: T, + pre_write: Option Result<(), std::io::Error> + Send + 'a>>, + post_write: Option>, + post_read: Option>, +} +impl<'a, T> IOHook<'a, T> { + pub fn new(io: T) -> Self { + Self { + io, + pre_write: None, + post_write: None, + post_read: None, + } + } + pub fn into_inner(self) -> T { + self.io + } + pub fn pre_write Result<(), std::io::Error> + Send + 'a>(&mut self, f: F) { + self.pre_write = Some(Box::new(f)) + } + pub fn post_write(&mut self, f: F) { + self.post_write = Some(Box::new(f)) + } + pub fn post_read(&mut self, f: F) { + self.post_read = Some(Box::new(f)) + } +} +impl<'a, T: AsyncWrite> AsyncWrite for IOHook<'a, T> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + if let Some(pre_write) = this.pre_write { + pre_write(buf)?; + } + let written = futures::ready!(this.io.poll_write(cx, buf)?); + if let Some(post_write) = this.post_write { + post_write(&buf[..written]); + } + Poll::Ready(Ok(written)) + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().io.poll_flush(cx) + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().io.poll_shutdown(cx) + } +} +impl<'a, T: AsyncRead> AsyncRead for IOHook<'a, T> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let start = buf.filled().len(); + futures::ready!(this.io.poll_read(cx, buf)?); + if let Some(post_read) = this.post_read { + post_read(&buf.filled()[start..]); + } + Poll::Ready(Ok(())) + } +} + #[pin_project::pin_project] pub struct BufferedWriteReader { #[pin] @@ -311,6 +394,7 @@ impl AsyncRead for BufferedWriteReader { pub trait CursorExt { fn pure_read(&mut self, buf: &mut ReadBuf<'_>); + fn remaining_slice(&self) -> &[u8]; } impl> CursorExt for Cursor { @@ -323,109 +407,197 @@ impl> CursorExt for Cursor { buf.put_slice(&self.get_ref().as_ref()[self.position() as usize..end]); self.set_position(end as u64); } + fn remaining_slice(&self) -> &[u8] { + let len = self.position().min(self.get_ref().as_ref().len() as u64); + &self.get_ref().as_ref()[(len as usize)..] + } +} + +#[derive(Debug)] +enum BTBuffer { + NotBuffering, + Buffering { read: Vec, write: Vec }, + Rewound { read: Cursor> }, +} +impl Default for BTBuffer { + fn default() -> Self { + BTBuffer::NotBuffering + } } #[pin_project::pin_project] #[derive(Debug)] -pub struct BackTrackingReader { +pub struct BackTrackingIO { #[pin] - reader: T, - buffer: Cursor>, - buffering: bool, + io: T, + buffer: BTBuffer, } -impl BackTrackingReader { - pub fn new(reader: T) -> Self { +impl BackTrackingIO { + pub fn new(io: T) -> Self { Self { - reader, - buffer: Cursor::new(Vec::new()), - buffering: false, + io, + buffer: BTBuffer::Buffering { + read: Vec::new(), + write: Vec::new(), + }, } } - pub fn start_buffering(&mut self) { - self.buffer.set_position(0); - self.buffer.get_mut().truncate(0); - self.buffering = true; + pub fn read_buffer(&self) -> &[u8] { + match &self.buffer { + BTBuffer::NotBuffering => &[], + BTBuffer::Buffering { read, .. } => read, + BTBuffer::Rewound { read } => read.remaining_slice(), + } } - pub fn stop_buffering(&mut self) { - self.buffer.set_position(0); - self.buffer.get_mut().truncate(0); - self.buffering = false; + #[must_use] + pub fn stop_buffering(&mut self) -> Vec { + match std::mem::take(&mut self.buffer) { + BTBuffer::Buffering { write, .. } => write, + BTBuffer::NotBuffering => Vec::new(), + BTBuffer::Rewound { read } => { + self.buffer = BTBuffer::Rewound { read }; + Vec::new() + } + } } - pub fn rewind(&mut self) { - self.buffering = false; + pub fn rewind<'a>(&'a mut self) -> (Vec, &'a [u8]) { + match std::mem::take(&mut self.buffer) { + BTBuffer::Buffering { read, write } => { + self.buffer = BTBuffer::Rewound { + read: Cursor::new(read), + }; + ( + write, + match &self.buffer { + BTBuffer::Rewound { read } => read.get_ref(), + _ => unreachable!(), + }, + ) + } + BTBuffer::NotBuffering => (Vec::new(), &[]), + BTBuffer::Rewound { read } => { + self.buffer = BTBuffer::Rewound { read }; + ( + Vec::new(), + match &self.buffer { + BTBuffer::Rewound { read } => read.get_ref(), + _ => unreachable!(), + }, + ) + } + } } pub fn unwrap(self) -> T { - self.reader + self.io } } -impl AsyncRead for BackTrackingReader { +impl AsyncRead for BackTrackingIO { fn poll_read( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let this = self.project(); - if *this.buffering { - let filled = buf.filled().len(); - let res = this.reader.poll_read(cx, buf); - this.buffer - .get_mut() - .extend_from_slice(&buf.filled()[filled..]); - res - } else { - let mut ready = false; - if (this.buffer.position() as usize) < this.buffer.get_ref().len() { - this.buffer.pure_read(buf); - ready = true; + match this.buffer { + BTBuffer::Buffering { read, .. } => { + let filled = buf.filled().len(); + let res = this.io.poll_read(cx, buf); + read.extend_from_slice(&buf.filled()[filled..]); + res } - if buf.remaining() > 0 { - match this.reader.poll_read(cx, buf) { - Poll::Pending => { - if ready { - Poll::Ready(Ok(())) - } else { - Poll::Pending + BTBuffer::NotBuffering => this.io.poll_read(cx, buf), + BTBuffer::Rewound { read } => { + let mut ready = false; + if (read.position() as usize) < read.get_ref().len() { + read.pure_read(buf); + ready = true; + } + if buf.remaining() > 0 { + match this.io.poll_read(cx, buf) { + Poll::Pending => { + if ready { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } } + a => a, } - a => a, + } else { + Poll::Ready(Ok(())) } - } else { - Poll::Ready(Ok(())) + } + } + } +} +impl std::io::Read for BackTrackingIO { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match &mut self.buffer { + BTBuffer::Buffering { read, .. } => { + let n = self.io.read(buf)?; + read.extend_from_slice(&buf[..n]); + Ok(n) + } + BTBuffer::NotBuffering => self.io.read(buf), + BTBuffer::Rewound { read } => { + if (read.position() as usize) < read.get_ref().len() { + let n = std::io::Read::read(read, buf)?; + if n != 0 { + return Ok(n); + } + } + self.io.read(buf) } } } } -impl AsyncWrite for BackTrackingReader { +impl AsyncWrite for BackTrackingIO { fn is_write_vectored(&self) -> bool { - self.reader.is_write_vectored() + self.io.is_write_vectored() } fn poll_flush( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - self.project().reader.poll_flush(cx) + self.project().io.poll_flush(cx) } fn poll_shutdown( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - self.project().reader.poll_shutdown(cx) + self.project().io.poll_shutdown(cx) } fn poll_write( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &[u8], ) -> Poll> { - self.project().reader.poll_write(cx, buf) + let this = self.project(); + if let BTBuffer::Buffering { write, .. } = this.buffer { + write.extend_from_slice(buf); + Poll::Ready(Ok(buf.len())) + } else { + this.io.poll_write(cx, buf) + } } fn poll_write_vectored( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, bufs: &[std::io::IoSlice<'_>], ) -> Poll> { - self.project().reader.poll_write_vectored(cx, bufs) + let this = self.project(); + if let BTBuffer::Buffering { write, .. } = this.buffer { + let len = bufs.iter().map(|b| b.len()).sum(); + write.reserve(len); + for buf in bufs { + write.extend_from_slice(buf); + } + Poll::Ready(Ok(len)) + } else { + this.io.poll_write_vectored(cx, bufs) + } } } @@ -524,13 +696,13 @@ pub fn dir_copy<'a, P0: AsRef + 'a + Send + Sync, P1: AsRef + 'a + S let src_path = e.path(); let dst_path = dst_path.join(e.file_name()); if m.is_file() { - let mut dst_file = tokio::fs::File::create(&dst_path).await.with_ctx(|_| { + let mut dst_file = create_file(&dst_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, format!("create {}", dst_path.display()), ) })?; - let mut rdr = tokio::fs::File::open(&src_path).await.with_ctx(|_| { + let mut rdr = open_file(&src_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, format!("open {}", src_path.display()), @@ -620,16 +792,16 @@ impl AsyncRead for TimeoutStream { buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { let mut this = self.project(); - if let std::task::Poll::Ready(_) = this.sleep.as_mut().poll(cx) { + let timeout = this.sleep.as_mut().poll(cx); + let res = this.stream.poll_read(cx, buf); + if res.is_ready() { + this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { return std::task::Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::TimedOut, "timed out", ))); } - let res = this.stream.poll_read(cx, buf); - if res.is_ready() { - this.sleep.reset(Instant::now() + *this.timeout); - } res } } @@ -639,10 +811,16 @@ impl AsyncWrite for TimeoutStream { cx: &mut std::task::Context<'_>, buf: &[u8], ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_write(cx, buf); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } @@ -650,10 +828,16 @@ impl AsyncWrite for TimeoutStream { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_flush(cx); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } @@ -661,11 +845,526 @@ impl AsyncWrite for TimeoutStream { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_shutdown(cx); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } } + +#[derive(Debug)] +pub struct TmpDir { + path: PathBuf, +} +impl TmpDir { + pub async fn new() -> Result { + let path = Path::new("/var/tmp/startos").join(base32::encode( + base32::Alphabet::Rfc4648 { padding: false }, + &rand::random::<[u8; 8]>(), + )); + if tokio::fs::metadata(&path).await.is_ok() { + return Err(Error::new( + eyre!("{path:?} already exists"), + ErrorKind::Filesystem, + )); + } + tokio::fs::create_dir_all(&path).await?; + Ok(Self { path }) + } + + pub async fn delete(self) -> Result<(), Error> { + tokio::fs::remove_dir_all(&self.path).await?; + Ok(()) + } + + pub async fn gc(self: Arc) -> Result<(), Error> { + if let Ok(dir) = Arc::try_unwrap(self) { + dir.delete().await + } else { + Ok(()) + } + } +} +impl std::ops::Deref for TmpDir { + type Target = Path; + fn deref(&self) -> &Self::Target { + &self.path + } +} +impl AsRef for TmpDir { + fn as_ref(&self) -> &Path { + &*self + } +} +impl Drop for TmpDir { + fn drop(&mut self) { + if self.path.exists() { + let path = std::mem::take(&mut self.path); + tokio::spawn(async move { + tokio::fs::remove_dir_all(&path).await.log_err(); + }); + } + } +} + +pub async fn open_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + File::open(path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("open {path:?}"))) +} + +pub async fn create_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?; + } + File::create(path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}"))) +} + +pub async fn append_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?; + } + OpenOptions::new() + .create(true) + .append(true) + .open(path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}"))) +} + +pub async fn delete_file(path: impl AsRef) -> Result<(), Error> { + let path = path.as_ref(); + tokio::fs::remove_file(path) + .await + .or_else(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(()) + } else { + Err(e) + } + }) + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("delete {path:?}"))) +} + +pub async fn rename(src: impl AsRef, dst: impl AsRef) -> Result<(), Error> { + let src = src.as_ref(); + let dst = dst.as_ref(); + if let Some(parent) = dst.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?; + } + tokio::fs::rename(src, dst) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mv {src:?} -> {dst:?}"))) +} + +fn poll_flush_prefix( + mut writer: Pin<&mut W>, + cx: &mut std::task::Context<'_>, + prefix: &mut VecDeque>>, + flush_writer: bool, +) -> Poll> { + while let Some(mut cur) = prefix.pop_front() { + let buf = CursorExt::remaining_slice(&cur); + if !buf.is_empty() { + match writer.as_mut().poll_write(cx, buf)? { + Poll::Ready(n) if n == buf.len() => (), + Poll::Ready(n) => { + cur.advance(n); + prefix.push_front(cur); + } + Poll::Pending => { + prefix.push_front(cur); + return Poll::Pending; + } + } + } + } + if flush_writer { + writer.poll_flush(cx) + } else { + Poll::Ready(Ok(())) + } +} + +fn poll_write_prefix_buf( + mut writer: Pin<&mut W>, + cx: &mut std::task::Context<'_>, + prefix: &mut VecDeque>>, + buf: &[u8], +) -> Poll> { + futures::ready!(poll_flush_prefix(writer.as_mut(), cx, prefix, false)?); + writer.poll_write(cx, buf) +} + +#[pin_project::pin_project] +pub struct TeeWriter { + capacity: usize, + buffer1: VecDeque>>, + buffer2: VecDeque>>, + #[pin] + writer1: W1, + #[pin] + writer2: W2, +} +impl TeeWriter { + pub fn new(writer1: W1, writer2: W2, capacity: usize) -> Self { + Self { + capacity, + buffer1: VecDeque::new(), + buffer2: VecDeque::new(), + writer1, + writer2, + } + } +} +impl TeeWriter { + pub async fn into_inner(mut self) -> Result<(W1, W2), Error> { + self.flush().await?; + + Ok((self.writer1, self.writer2)) + } +} +impl AsyncWrite for TeeWriter { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + mut buf: &[u8], + ) -> Poll> { + let mut this = self.project(); + let buffer_size = this + .buffer1 + .iter() + .chain(this.buffer2.iter()) + .map(|b| b.get_ref().len()) + .sum::(); + if buffer_size < *this.capacity { + let to_write = std::cmp::min(*this.capacity - buffer_size, buf.len()); + buf = &buf[0..to_write]; + } else { + match ( + poll_flush_prefix(this.writer1.as_mut(), cx, &mut this.buffer1, false)?, + poll_flush_prefix(this.writer2.as_mut(), cx, &mut this.buffer2, false)?, + ) { + (Poll::Ready(()), Poll::Ready(())) => (), + _ => return Poll::Pending, + } + } + let (w1, w2) = match ( + poll_write_prefix_buf(this.writer1.as_mut(), cx, &mut this.buffer1, buf)?, + poll_write_prefix_buf(this.writer2.as_mut(), cx, &mut this.buffer2, buf)?, + ) { + (Poll::Pending, Poll::Pending) => return Poll::Pending, + (Poll::Ready(n), Poll::Pending) => (n, 0), + (Poll::Pending, Poll::Ready(n)) => (0, n), + (Poll::Ready(n1), Poll::Ready(n2)) => (n1, n2), + }; + if w1 > w2 { + this.buffer2.push_back(Cursor::new(buf[w2..w1].to_vec())); + } else if w1 < w2 { + this.buffer1.push_back(Cursor::new(buf[w1..w2].to_vec())); + } + Poll::Ready(Ok(std::cmp::max(w1, w2))) + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let mut this = self.project(); + match ( + poll_flush_prefix(this.writer1, cx, &mut this.buffer1, true)?, + poll_flush_prefix(this.writer2, cx, &mut this.buffer2, true)?, + ) { + (Poll::Ready(()), Poll::Ready(())) => Poll::Ready(Ok(())), + _ => Poll::Pending, + } + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.poll_flush(cx) + } +} + +#[pin_project::pin_project] +pub struct ParallelBlake3Writer { + #[pin] + hasher: NonDetachingJoinHandle, + buffer: Arc<(std::sync::Mutex<(BytesMut, Vec, bool)>, Notify)>, + capacity: usize, +} +impl ParallelBlake3Writer { + /// memory usage can be as much as 2x capacity + pub fn new(capacity: usize) -> Self { + let buffer = Arc::new(( + std::sync::Mutex::new((BytesMut::new(), Vec::::new(), false)), + Notify::new(), + )); + let hasher_buffer = buffer.clone(); + Self { + hasher: tokio::spawn(async move { + let mut hasher = blake3::Hasher::new(); + let mut to_hash = BytesMut::new(); + let mut notified; + while { + let mut guard = hasher_buffer.0.lock().unwrap(); + let (buffer, wakers, shutdown) = &mut *guard; + std::mem::swap(buffer, &mut to_hash); + let wakers = std::mem::take(wakers); + let shutdown = *shutdown; + notified = hasher_buffer.1.notified(); + drop(guard); + if to_hash.len() > 128 * 1024 + /* 128 KiB */ + { + hasher.update_rayon(&to_hash); + } else { + hasher.update(&to_hash); + } + to_hash.truncate(0); + for waker in wakers { + waker.wake(); + } + !shutdown && to_hash.len() == 0 + } { + notified.await; + } + hasher.finalize() + }) + .into(), + buffer, + capacity, + } + } + + pub async fn finalize(mut self) -> Result { + self.shutdown().await?; + self.hasher.await.with_kind(ErrorKind::Unknown) + } +} +impl AsyncWrite for ParallelBlake3Writer { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + let mut guard = this.buffer.0.lock().map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, eyre!("hashing thread panicked")) + })?; + let (buffer, wakers, shutdown) = &mut *guard; + if !*shutdown { + if buffer.len() < *this.capacity { + let to_write = std::cmp::min(*this.capacity - buffer.len(), buf.len()); + buffer.extend_from_slice(&buf[0..to_write]); + if buffer.len() >= *this.capacity / 2 { + this.buffer.1.notify_waiters(); + } + Poll::Ready(Ok(to_write)) + } else { + wakers.push(cx.waker().clone()); + Poll::Pending + } + } else { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + eyre!("write after shutdown"), + ))) + } + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + let mut guard = this.buffer.0.lock().map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, eyre!("hashing thread panicked")) + })?; + let (buffer, wakers, _) = &mut *guard; + if buffer.is_empty() { + Poll::Ready(Ok(())) + } else { + wakers.push(cx.waker().clone()); + this.buffer.1.notify_waiters(); + Poll::Pending + } + } + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + futures::ready!(self.as_mut().poll_flush(cx)?); + let this = self.project(); + let mut guard = this.buffer.0.lock().map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, eyre!("hashing thread panicked")) + })?; + let (buffer, wakers, shutdown) = &mut *guard; + if *shutdown && buffer.len() == 0 { + return Poll::Ready(Ok(())); + } + wakers.push(cx.waker().clone()); + *shutdown = true; + this.buffer.1.notify_waiters(); + Poll::Pending + } +} + +#[pin_project::pin_project] +pub struct TrackingIO { + position: u64, + #[pin] + io: T, +} +impl TrackingIO { + pub fn new(start: u64, io: T) -> Self { + Self { + position: start, + io, + } + } + pub fn position(&self) -> u64 { + self.position + } + pub fn into_inner(self) -> T { + self.io + } +} +impl AsyncWrite for TrackingIO { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let this = self.project(); + let written = futures::ready!(this.io.poll_write(cx, buf)?); + *this.position += written as u64; + Poll::Ready(Ok(written)) + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().io.poll_flush(cx) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().io.poll_shutdown(cx) + } +} +impl AsyncRead for TrackingIO { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let start = buf.filled().len(); + futures::ready!(this.io.poll_read(cx, buf)?); + *this.position += (buf.filled().len() - start) as u64; + Poll::Ready(Ok(())) + } +} +impl std::cmp::PartialEq for TrackingIO { + fn eq(&self, other: &Self) -> bool { + self.position.eq(&other.position) + } +} +impl std::cmp::Eq for TrackingIO {} +impl std::cmp::PartialOrd for TrackingIO { + fn partial_cmp(&self, other: &Self) -> Option { + self.position.partial_cmp(&other.position) + } +} +impl std::cmp::Ord for TrackingIO { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.position.cmp(&other.position) + } +} +impl std::borrow::Borrow for TrackingIO { + fn borrow(&self) -> &u64 { + &self.position + } +} + +pub struct MutexIO(OwnedMutexGuard); +impl AsyncRead for MutexIO { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_read(cx, buf) + } +} +impl AsyncWrite for MutexIO { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_write(cx, buf) + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_flush(cx) + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_shutdown(cx) + } +} + +#[pin_project::pin_project] +pub struct AsyncReadStream { + buffer: Vec>, + #[pin] + pub io: T, +} +impl AsyncReadStream { + pub fn new(io: T, buffer_size: usize) -> Self { + Self { + buffer: vec![MaybeUninit::uninit(); buffer_size], + io, + } + } +} +impl Stream for AsyncReadStream { + type Item = Result, Error>; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + let mut buf = ReadBuf::uninit(this.buffer); + match futures::ready!(this.io.poll_read(cx, &mut buf)) { + Ok(()) if buf.filled().is_empty() => Poll::Ready(None), + Ok(()) => Poll::Ready(Some(Ok(buf.filled().to_vec()))), + Err(e) => Poll::Ready(Some(Err(e.into()))), + } + } +} diff --git a/core/startos/src/util/logger.rs b/core/startos/src/util/logger.rs index c7ab41ba2..816721d5b 100644 --- a/core/startos/src/util/logger.rs +++ b/core/startos/src/util/logger.rs @@ -1,11 +1,62 @@ +use std::fs::File; +use std::io::{self, Write}; +use std::sync::{Arc, Mutex, MutexGuard}; + +use lazy_static::lazy_static; use tracing::Subscriber; +use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::util::SubscriberInitExt; +lazy_static! { + pub static ref LOGGER: StartOSLogger = StartOSLogger::init(); +} + #[derive(Clone)] -pub struct EmbassyLogger {} +pub struct StartOSLogger { + logfile: LogFile, +} + +#[derive(Clone, Default)] +struct LogFile(Arc>>); +impl<'a> MakeWriter<'a> for LogFile { + type Writer = Box; + fn make_writer(&'a self) -> Self::Writer { + let f = self.0.lock().unwrap(); + if f.is_some() { + struct TeeWriter<'a>(MutexGuard<'a, Option>); + impl<'a> Write for TeeWriter<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + let n = if let Some(f) = &mut *self.0 { + f.write(buf)? + } else { + buf.len() + }; + io::stderr().write_all(&buf[..n])?; + Ok(n) + } + fn flush(&mut self) -> io::Result<()> { + if let Some(f) = &mut *self.0 { + f.flush()?; + } + Ok(()) + } + } + Box::new(TeeWriter(f)) + } else { + drop(f); + Box::new(io::stderr()) + } + } +} + +impl StartOSLogger { + pub fn enable(&self) {} + + pub fn set_logfile(&self, logfile: Option) { + *self.logfile.0.lock().unwrap() = logfile; + } -impl EmbassyLogger { - fn base_subscriber() -> impl Subscriber { + fn base_subscriber(logfile: LogFile) -> impl Subscriber { use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; @@ -21,7 +72,11 @@ impl EmbassyLogger { let filter_layer = filter_layer .add_directive("tokio=trace".parse().unwrap()) .add_directive("runtime=trace".parse().unwrap()); - let fmt_layer = fmt::layer().with_target(true); + let fmt_layer = fmt::layer() + .with_writer(logfile) + .with_line_number(true) + .with_file(true) + .with_target(true); let sub = tracing_subscriber::registry() .with(filter_layer) @@ -33,11 +88,12 @@ impl EmbassyLogger { sub } - pub fn init() -> Self { - Self::base_subscriber().init(); + fn init() -> Self { + let logfile = LogFile::default(); + Self::base_subscriber(logfile.clone()).init(); color_eyre::install().unwrap_or_else(|_| tracing::warn!("tracing too many times")); - EmbassyLogger {} + StartOSLogger { logfile } } } diff --git a/core/startos/src/util/lshw.rs b/core/startos/src/util/lshw.rs index dd260f644..df5fff5f8 100644 --- a/core/startos/src/util/lshw.rs +++ b/core/startos/src/util/lshw.rs @@ -1,14 +1,16 @@ use models::{Error, ResultExt}; use serde::{Deserialize, Serialize}; use tokio::process::Command; +use ts_rs::TS; use crate::util::Invoke; const KNOWN_CLASSES: &[&str] = &["processor", "display"]; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(tag = "class")] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] pub enum LshwDevice { Processor(LshwProcessor), Display(LshwDisplay), @@ -28,12 +30,12 @@ impl LshwDevice { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct LshwProcessor { pub product: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct LshwDisplay { pub product: String, } diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 2683f23c8..d6f426135 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -1,38 +1,54 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; +use std::fmt; use std::future::Future; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::Stdio; +use std::str::FromStr; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; +use ::serde::{Deserialize, Serialize}; use async_trait::async_trait; -use clap::ArgMatches; use color_eyre::eyre::{self, eyre}; use fd_lock_rs::FdLock; +use futures::future::BoxFuture; +use futures::FutureExt; use helpers::canonicalize; pub use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; use lazy_static::lazy_static; -pub use models::Version; +pub use models::VersionString; use pin_project::pin_project; use sha2::Digest; use tokio::fs::File; -use tokio::sync::{Mutex, OwnedMutexGuard, RwLock}; +use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; +use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tracing::instrument; +use ts_rs::TS; +use url::Url; use crate::shutdown::Shutdown; +use crate::util::io::create_file; +use crate::util::serde::{deserialize_from_str, serialize_display}; use crate::{Error, ErrorKind, ResultExt as _}; -pub mod config; + +pub mod actor; +pub mod collections; pub mod cpupower; pub mod crypto; -pub mod docker; +pub mod future; pub mod http_reader; pub mod io; pub mod logger; pub mod lshw; +pub mod net; +pub mod rpc; +pub mod rpc_client; pub mod serde; +pub mod sync; #[derive(Clone, Copy, Debug, ::serde::Deserialize, ::serde::Serialize)] pub enum Never {} @@ -48,25 +64,50 @@ impl std::fmt::Display for Never { } } impl std::error::Error for Never {} +impl AsRef for Never { + fn as_ref(&self) -> &T { + match *self {} + } +} -#[async_trait::async_trait] pub trait Invoke<'a> { type Extended<'ext> where Self: 'ext, 'ext: 'a; + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext>; fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext>; fn input<'ext: 'a, Input: tokio::io::AsyncRead + Unpin + Send>( &'ext mut self, input: Option<&'ext mut Input>, ) -> Self::Extended<'ext>; - async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error>; + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext>; + fn invoke( + &mut self, + error_kind: crate::ErrorKind, + ) -> impl Future, Error>> + Send; } pub struct ExtendedCommand<'a> { cmd: &'a mut tokio::process::Command, timeout: Option, - input: Option<&'a mut (dyn tokio::io::AsyncRead + Unpin + Send)>, + input: Option<&'a mut (dyn AsyncRead + Unpin + Send)>, + pipe: VecDeque<&'a mut tokio::process::Command>, + capture: bool, +} +impl<'a> From<&'a mut tokio::process::Command> for ExtendedCommand<'a> { + fn from(value: &'a mut tokio::process::Command) -> Self { + ExtendedCommand { + cmd: value, + timeout: None, + input: None, + pipe: VecDeque::new(), + capture: true, + } + } } impl<'a> std::ops::Deref for ExtendedCommand<'a> { type Target = tokio::process::Command; @@ -80,50 +121,60 @@ impl<'a> std::ops::DerefMut for ExtendedCommand<'a> { } } -#[async_trait::async_trait] impl<'a> Invoke<'a> for tokio::process::Command { - type Extended<'ext> = ExtendedCommand<'ext> + type Extended<'ext> + = ExtendedCommand<'ext> where Self: 'ext, 'ext: 'a; + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext> { + let mut cmd = ExtendedCommand::from(self); + cmd.pipe.push_back(next); + cmd + } fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext> { - ExtendedCommand { - cmd: self, - timeout, - input: None, - } + let mut cmd = ExtendedCommand::from(self); + cmd.timeout = timeout; + cmd } - fn input<'ext: 'a, Input: tokio::io::AsyncRead + Unpin + Send>( + fn input<'ext: 'a, Input: AsyncRead + Unpin + Send>( &'ext mut self, input: Option<&'ext mut Input>, ) -> Self::Extended<'ext> { - ExtendedCommand { - cmd: self, - timeout: None, - input: if let Some(input) = input { - Some(&mut *input) - } else { - None - }, - } + let mut cmd = ExtendedCommand::from(self); + cmd.input = if let Some(input) = input { + Some(&mut *input) + } else { + None + }; + cmd + } + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext> { + let mut cmd = ExtendedCommand::from(self); + cmd.capture = capture; + cmd } async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error> { - ExtendedCommand { - cmd: self, - timeout: None, - input: None, - } - .invoke(error_kind) - .await + ExtendedCommand::from(self).invoke(error_kind).await } } -#[async_trait::async_trait] impl<'a> Invoke<'a> for ExtendedCommand<'a> { - type Extended<'ext> = &'ext mut ExtendedCommand<'ext> + type Extended<'ext> + = &'ext mut ExtendedCommand<'ext> where Self: 'ext, 'ext: 'a; + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext> { + self.pipe.push_back(next.kill_on_drop(true)); + self + } fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext> { self.timeout = timeout; self @@ -139,39 +190,146 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> { }; self } + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext> { + self.capture = capture; + self + } + #[instrument(skip_all)] async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error> { + let cmd_str = self + .cmd + .as_std() + .get_program() + .to_string_lossy() + .into_owned(); self.cmd.kill_on_drop(true); if self.input.is_some() { self.cmd.stdin(Stdio::piped()); } - self.cmd.stdout(Stdio::piped()); - self.cmd.stderr(Stdio::piped()); - let mut child = self.cmd.spawn()?; - if let (Some(mut stdin), Some(input)) = (child.stdin.take(), self.input.take()) { - use tokio::io::AsyncWriteExt; - tokio::io::copy(input, &mut stdin).await?; - stdin.flush().await?; - stdin.shutdown().await?; - drop(stdin); + if self.pipe.is_empty() { + if self.capture { + self.cmd.stdout(Stdio::piped()); + self.cmd.stderr(Stdio::piped()); + } + let mut child = self.cmd.spawn().with_ctx(|_| (error_kind, &cmd_str))?; + if let (Some(mut stdin), Some(input)) = (child.stdin.take(), self.input.take()) { + use tokio::io::AsyncWriteExt; + tokio::io::copy(input, &mut stdin).await?; + stdin.flush().await?; + stdin.shutdown().await?; + drop(stdin); + } + let res = match self.timeout { + None => child + .wait_with_output() + .await + .with_ctx(|_| (error_kind, &cmd_str))?, + Some(t) => tokio::time::timeout(t, child.wait_with_output()) + .await + .with_kind(ErrorKind::Timeout)? + .with_ctx(|_| (error_kind, &cmd_str))?, + }; + crate::ensure_code!( + res.status.success(), + error_kind, + "{}", + Some(&res.stderr) + .filter(|a| !a.is_empty()) + .or(Some(&res.stdout)) + .filter(|a| !a.is_empty()) + .and_then(|a| std::str::from_utf8(a).ok()) + .unwrap_or(&format!("{} exited with code {}", cmd_str, res.status)) + ); + Ok(res.stdout) + } else { + let mut futures = Vec::>>::new(); // todo: predict capacity + + let mut cmds = std::mem::take(&mut self.pipe); + cmds.push_front(&mut *self.cmd); + let len = cmds.len(); + + let timeout = self.timeout; + + let mut prev = self + .input + .take() + .map(|i| Box::new(i) as Box); + for (idx, cmd) in IntoIterator::into_iter(cmds).enumerate() { + let last = idx == len - 1; + if self.capture || !last { + cmd.stdout(Stdio::piped()); + } + if self.capture { + cmd.stderr(Stdio::piped()); + } + if prev.is_some() { + cmd.stdin(Stdio::piped()); + } + let mut child = cmd.spawn().with_ctx(|_| (error_kind, &cmd_str))?; + let input = std::mem::replace( + &mut prev, + child + .stdout + .take() + .map(|i| Box::new(BufReader::new(i)) as Box), + ); + futures.push( + async move { + if let (Some(mut stdin), Some(mut input)) = (child.stdin.take(), input) { + use tokio::io::AsyncWriteExt; + tokio::io::copy(&mut input, &mut stdin).await?; + stdin.flush().await?; + stdin.shutdown().await?; + drop(stdin); + } + let res = match timeout { + None => child.wait_with_output().await?, + Some(t) => tokio::time::timeout(t, child.wait_with_output()) + .await + .with_kind(ErrorKind::Timeout)??, + }; + crate::ensure_code!( + res.status.success(), + error_kind, + "{}", + Some(&res.stderr) + .filter(|a| !a.is_empty()) + .or(Some(&res.stdout)) + .filter(|a| !a.is_empty()) + .and_then(|a| std::str::from_utf8(a).ok()) + .unwrap_or(&format!( + "{} exited with code {}", + cmd.as_std().get_program().to_string_lossy(), + res.status + )) + ); + + Ok(()) + } + .boxed(), + ); + } + + let (send, recv) = oneshot::channel(); + futures.push( + async move { + if let Some(mut prev) = prev { + let mut res = Vec::new(); + prev.read_to_end(&mut res).await?; + send.send(res).unwrap(); + } else { + send.send(Vec::new()).unwrap(); + } + + Ok(()) + } + .boxed(), + ); + + futures::future::try_join_all(futures).await?; + + Ok(recv.await.unwrap()) } - let res = match self.timeout { - None => child.wait_with_output().await?, - Some(t) => tokio::time::timeout(t, child.wait_with_output()) - .await - .with_kind(ErrorKind::Timeout)??, - }; - crate::ensure_code!( - res.status.success(), - error_kind, - "{}", - Some(&res.stderr) - .filter(|a| !a.is_empty()) - .or(Some(&res.stdout)) - .filter(|a| !a.is_empty()) - .and_then(|a| std::str::from_utf8(a).ok()) - .unwrap_or(&format!("Unknown Error ({})", res.status)) - ); - Ok(res.stdout) } } @@ -234,16 +392,16 @@ impl SOption for SNone {} #[async_trait] pub trait AsyncFileExt: Sized { - async fn maybe_open + Send + Sync>(path: P) -> std::io::Result>; + async fn maybe_open + Send + Sync>(path: P) -> Result, Error>; async fn delete + Send + Sync>(path: P) -> std::io::Result<()>; } #[async_trait] impl AsyncFileExt for File { - async fn maybe_open + Send + Sync>(path: P) -> std::io::Result> { - match File::open(path).await { + async fn maybe_open + Send + Sync>(path: P) -> Result, Error> { + match File::open(path.as_ref()).await { Ok(f) => Ok(Some(f)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(e), + Err(e) => Err(e).with_ctx(|_| (ErrorKind::Filesystem, path.as_ref().display())), } } async fn delete + Send + Sync>(path: P) -> std::io::Result<()> { @@ -275,8 +433,6 @@ impl std::io::Write for FmtWriter { } } -pub fn display_none(_: T, _: &ArgMatches) {} - pub struct Container(RwLock>); impl Container { pub fn new(value: Option) -> Self { @@ -405,11 +561,11 @@ impl T, T> Drop for GeneralGuard { } } -pub struct FileLock(OwnedMutexGuard<()>, Option>); +pub struct FileLock(#[allow(unused)] OwnedMutexGuard<()>, Option>); impl Drop for FileLock { fn drop(&mut self) { if let Some(fd_lock) = self.1.take() { - tokio::task::spawn_blocking(|| fd_lock.unlock(true).map_err(|(_, e)| e).unwrap()); + tokio::task::spawn_blocking(|| fd_lock.unlock(true).map_err(|(_, e)| e).log_err()); } } } @@ -441,9 +597,7 @@ impl FileLock { .await .with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?; } - let f = File::create(&path) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, path.display().to_string()))?; + let f = create_file(&path).await?; let file_guard = tokio::task::spawn_blocking(move || { fd_lock_rs::FdLock::lock(f, fd_lock_rs::LockType::Exclusive, blocking) }) @@ -466,3 +620,82 @@ impl FileLock { pub fn assure_send(x: T) -> T { x } + +pub enum MaybeOwned<'a, T> { + Borrowed(&'a T), + Owned(T), +} +impl<'a, T> std::ops::Deref for MaybeOwned<'a, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + match self { + Self::Borrowed(a) => *a, + Self::Owned(a) => a, + } + } +} +impl<'a, T> From for MaybeOwned<'a, T> { + fn from(value: T) -> Self { + MaybeOwned::Owned(value) + } +} +impl<'a, T> From<&'a T> for MaybeOwned<'a, T> { + fn from(value: &'a T) -> Self { + MaybeOwned::Borrowed(value) + } +} + +pub fn new_guid() -> InternedString { + use rand::RngCore; + let mut buf = [0; 20]; + rand::thread_rng().fill_bytes(&mut buf); + InternedString::intern(base32::encode( + base32::Alphabet::Rfc4648 { padding: false }, + &buf, + )) +} + +#[derive(Debug, Clone, TS)] +#[ts(type = "string")] +pub enum PathOrUrl { + Path(PathBuf), + Url(Url), +} +impl FromStr for PathOrUrl { + type Err = ::Err; + fn from_str(s: &str) -> Result { + if let Ok(url) = s.parse::() { + if let Some(path) = s.strip_prefix("file://") { + Ok(Self::Path(path.parse()?)) + } else { + Ok(Self::Url(url)) + } + } else { + Ok(Self::Path(s.parse()?)) + } + } +} +impl fmt::Display for PathOrUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Path(p) => write!(f, "file://{}", p.display()), + Self::Url(u) => write!(f, "{u}"), + } + } +} +impl<'de> Deserialize<'de> for PathOrUrl { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for PathOrUrl { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + serialize_display(self, serializer) + } +} diff --git a/core/startos/src/util/net.rs b/core/startos/src/util/net.rs new file mode 100644 index 000000000..1189f70f2 --- /dev/null +++ b/core/startos/src/util/net.rs @@ -0,0 +1,70 @@ +use core::fmt; +use std::borrow::Cow; +use std::sync::Mutex; + +use axum::extract::ws::{self, CloseFrame}; +use futures::{Future, Stream, StreamExt}; + +use crate::prelude::*; + +pub trait WebSocketExt { + fn normal_close( + self, + msg: impl Into> + Send, + ) -> impl Future> + Send; + fn close_result( + self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> impl Future> + Send; +} + +impl WebSocketExt for ws::WebSocket { + async fn normal_close(self, msg: impl Into> + Send) -> Result<(), Error> { + self.close_result(Ok::<_, Error>(msg)).await + } + async fn close_result( + mut self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> Result<(), Error> { + match result { + Ok(msg) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1000, + reason: msg.into(), + }))) + .await + .with_kind(ErrorKind::Network)?, + Err(e) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1011, + reason: e.to_string().into(), + }))) + .await + .with_kind(ErrorKind::Network)?, + } + while !matches!( + self.recv() + .await + .transpose() + .with_kind(ErrorKind::Network)?, + Some(ws::Message::Close(_)) | None + ) {} + Ok(()) + } +} + +pub struct SyncBody(Mutex); +impl From for SyncBody { + fn from(value: axum::body::Body) -> Self { + SyncBody(Mutex::new(value.into_data_stream())) + } +} +impl Stream for SyncBody { + type Item = ::Item; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.lock().unwrap().poll_next_unpin(cx) + } +} diff --git a/core/startos/src/util/rpc.rs b/core/startos/src/util/rpc.rs new file mode 100644 index 000000000..f7c91eb82 --- /dev/null +++ b/core/startos/src/util/rpc.rs @@ -0,0 +1,66 @@ +use std::path::Path; + +use clap::Parser; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::{open_file, ParallelBlake3Writer}; +use crate::util::serde::Base16; +use crate::util::{Apply, PathOrUrl}; +use crate::CAP_10_MiB; + +pub fn util() -> ParentHandler { + ParentHandler::new().subcommand( + "b3sum", + from_fn_async(b3sum).with_about("Calculate blake3 hash for a file"), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +pub struct B3sumParams { + #[arg(long = "no-mmap", action = clap::ArgAction::SetFalse)] + allow_mmap: bool, + file: String, +} + +pub async fn b3sum( + ctx: CliContext, + B3sumParams { file, allow_mmap }: B3sumParams, +) -> Result, Error> { + async fn b3sum_source(source: S) -> Result, Error> { + let mut hasher = ParallelBlake3Writer::new(CAP_10_MiB); + source.copy_all_to(&mut hasher).await?; + hasher.finalize().await.map(|h| *h.as_bytes()).map(Base16) + } + async fn b3sum_file( + path: impl AsRef, + allow_mmap: bool, + ) -> Result, Error> { + let file = MultiCursorFile::from(open_file(path).await?); + if allow_mmap { + return file.blake3_mmap().await.map(|h| *h.as_bytes()).map(Base16); + } + b3sum_source(file).await + } + match file.parse::()? { + PathOrUrl::Path(path) => b3sum_file(path, allow_mmap).await, + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + HttpSource::new(ctx.client.clone(), url) + .await? + .apply(b3sum_source) + .await + } else { + Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )) + } + } + } +} diff --git a/core/helpers/src/rpc_client.rs b/core/startos/src/util/rpc_client.rs similarity index 57% rename from core/helpers/src/rpc_client.rs rename to core/startos/src/util/rpc_client.rs index bdb505b40..82ce11e20 100644 --- a/core/helpers/src/rpc_client.rs +++ b/core/startos/src/util/rpc_client.rs @@ -5,17 +5,18 @@ use std::sync::{Arc, Weak}; use futures::future::BoxFuture; use futures::{FutureExt, TryFutureExt}; +use helpers::NonDetachingJoinHandle; use lazy_async_pool::Pool; use models::{Error, ErrorKind, ResultExt}; +use rpc_toolkit::yajrc::{self, Id, RpcError, RpcMethod, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; use tokio::net::UnixStream; use tokio::runtime::Handle; -use tokio::sync::{oneshot, Mutex}; -use yajrc::{Id, RpcError, RpcMethod, RpcRequest, RpcResponse}; +use tokio::sync::{oneshot, Mutex, OnceCell}; -use crate::NonDetachingJoinHandle; +use crate::util::io::TmpDir; type DynWrite = Box; type ResponseMap = BTreeMap>>; @@ -24,7 +25,7 @@ const MAX_TRIES: u64 = 3; pub struct RpcClient { id: Arc, - _handler: NonDetachingJoinHandle<()>, + handler: NonDetachingJoinHandle<()>, writer: DynWrite, responses: Weak>, } @@ -42,11 +43,11 @@ impl RpcClient { let weak_responses = Arc::downgrade(&responses); RpcClient { id, - _handler: tokio::spawn(async move { + handler: tokio::spawn(async move { let mut lines = BufReader::new(reader).lines(); while let Some(line) = lines.next_line().await.transpose() { match line.map_err(Error::from).and_then(|l| { - serde_json::from_str::(&l) + serde_json::from_str::(crate::dbg!(&l)) .with_kind(ErrorKind::Deserialization) }) { Ok(l) => { @@ -54,7 +55,7 @@ impl RpcClient { if let Some(res) = responses.lock().await.remove(&id) { if let Err(e) = res.send(l.result) { tracing::warn!( - "RpcClient Response for Unknown ID: {:?}", + "RpcClient Response after request aborted: {:?}", e ); } @@ -74,6 +75,14 @@ impl RpcClient { } } } + for (_, res) in std::mem::take(&mut *responses.lock().await) { + if let Err(e) = res.send(Err(RpcError { + data: Some("client disconnected before response received".into()), + ..yajrc::INTERNAL_ERROR + })) { + tracing::warn!("RpcClient Response after request aborted: {:?}", e); + } + } }) .into(), writer, @@ -105,10 +114,10 @@ impl RpcClient { let (send, recv) = oneshot::channel(); w.lock().await.insert(id.clone(), send); self.writer - .write_all((serde_json::to_string(&request)? + "\n").as_bytes()) + .write_all((crate::dbg!(serde_json::to_string(&request))? + "\n").as_bytes()) .await .map_err(|e| { - let mut err = yajrc::INTERNAL_ERROR.clone(); + let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone(); err.data = Some(json!(e.to_string())); err })?; @@ -123,14 +132,40 @@ impl RpcClient { } tracing::debug!( "Client has finished {:?}", - futures::poll!(&mut self._handler) + futures::poll!(&mut self.handler) ); - let mut err = yajrc::INTERNAL_ERROR.clone(); + let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone(); err.data = Some(json!("RpcClient thread has terminated")); Err(err) } + + pub async fn notify( + &mut self, + method: T, + params: T::Params, + ) -> Result<(), RpcError> + where + T: Serialize, + T::Params: Serialize, + { + let request = RpcRequest { + id: None, + method, + params, + }; + self.writer + .write_all((crate::dbg!(serde_json::to_string(&request))? + "\n").as_bytes()) + .await + .map_err(|e| { + let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone(); + err.data = Some(json!(e.to_string())); + err + })?; + Ok(()) + } } +#[derive(Clone)] pub struct UnixRpcClient { pool: Pool< RpcClient, @@ -141,18 +176,35 @@ pub struct UnixRpcClient { } impl UnixRpcClient { pub fn new(path: PathBuf) -> Self { + let tmpdir = Arc::new(OnceCell::new()); let rt = Handle::current(); let id = Arc::new(AtomicUsize::new(0)); Self { pool: Pool::new( 0, Box::new(move || { - let path = path.clone(); + let mut path = path.clone(); let id = id.clone(); - rt.spawn(async move { + let tmpdir = tmpdir.clone(); + NonDetachingJoinHandle::from(rt.spawn(async move { + if path.as_os_str().len() >= 108 + // libc::sockaddr_un.sun_path.len() + { + let new_path = tmpdir + .get_or_try_init(|| TmpDir::new()) + .await + .map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e.source) + })? + .join("link.sock"); + if tokio::fs::metadata(&new_path).await.is_err() { + tokio::fs::symlink(&path, &new_path).await?; + } + path = new_path; + } let (r, w) = UnixStream::connect(&path).await?.into_split(); Ok(RpcClient::new(w, r, id)) - }) + })) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) .and_then(|x| async move { x }) .boxed() @@ -173,15 +225,55 @@ impl UnixRpcClient { { let mut tries = 0; let res = loop { - tries += 1; let mut client = self.pool.clone().get().await?; + if client.handler.is_finished() { + client.destroy(); + continue; + } let res = client.request(method.clone(), params.clone()).await; match &res { - Err(e) if e.code == yajrc::INTERNAL_ERROR.code => { + Err(e) if e.code == rpc_toolkit::yajrc::INTERNAL_ERROR.code => { + let mut e = Error::from(e.clone()); + e.kind = ErrorKind::Filesystem; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + client.destroy(); + } + _ => break res, + } + tries += 1; + if tries > MAX_TRIES { + tracing::warn!("Max Tries exceeded"); + break res; + } + }; + res + } + + pub async fn notify(&self, method: T, params: T::Params) -> Result<(), RpcError> + where + T: Serialize + Clone, + T::Params: Serialize + Clone, + { + let mut tries = 0; + let res = loop { + let mut client = self.pool.clone().get().await?; + if client.handler.is_finished() { + client.destroy(); + continue; + } + let res = client.notify(method.clone(), params.clone()).await; + match &res { + Err(e) if e.code == rpc_toolkit::yajrc::INTERNAL_ERROR.code => { + let mut e = Error::from(e.clone()); + e.kind = ErrorKind::Filesystem; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); client.destroy(); } _ => break res, } + tries += 1; if tries > MAX_TRIES { tracing::warn!("Max Tries exceeded"); break res; diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 4a6f7551b..5b785ce9a 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1,16 +1,27 @@ +use std::collections::VecDeque; use std::marker::PhantomData; use std::ops::Deref; -use std::process::exit; use std::str::FromStr; -use clap::ArgMatches; +use base64::Engine; +use clap::builder::ValueParserFactory; +use clap::{ArgMatches, CommandFactory, FromArgMatches}; use color_eyre::eyre::eyre; +use imbl::OrdMap; +use models::FromStrParser; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use rpc_toolkit::{ + CliBindings, Context, HandlerArgs, HandlerArgsFor, HandlerFor, HandlerTypes, PrintCliResult, +}; +use serde::de::DeserializeOwned; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; +use ts_rs::TS; use super::IntoDoubleEndedIterator; -use crate::{Error, ResultExt}; +use crate::prelude::*; +use crate::util::Apply; pub fn deserialize_from_str< 'de, @@ -26,7 +37,11 @@ pub fn deserialize_from_str< { type Value = T; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "a parsable string") + write!( + formatter, + "a string that can be parsed as a {}", + std::any::type_name::() + ) } fn visit_str(self, v: &str) -> Result where @@ -96,64 +111,22 @@ pub fn serialize_display_opt( Option::::serialize(&t.as_ref().map(|t| t.to_string()), serializer) } -pub mod ed25519_pubkey { - use ed25519_dalek::VerifyingKey; - use serde::de::{Error, Unexpected, Visitor}; - use serde::{Deserializer, Serializer}; - - pub fn serialize( - pubkey: &VerifyingKey, - serializer: S, - ) -> Result { - serializer.serialize_str(&base32::encode( - base32::Alphabet::RFC4648 { padding: true }, - pubkey.as_bytes(), - )) - } - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result { - struct PubkeyVisitor; - impl<'de> Visitor<'de> for PubkeyVisitor { - type Value = ed25519_dalek::VerifyingKey; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "an RFC4648 encoded string") - } - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - VerifyingKey::from_bytes( - &<[u8; 32]>::try_from( - base32::decode(base32::Alphabet::RFC4648 { padding: true }, v).ok_or( - Error::invalid_value(Unexpected::Str(v), &"an RFC4648 encoded string"), - )?, - ) - .map_err(|e| Error::invalid_length(e.len(), &"32 bytes"))?, - ) - .map_err(Error::custom) - } - } - deserializer.deserialize_str(PubkeyVisitor) - } -} - #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum ValuePrimative { +pub enum ValuePrimitive { Null, Boolean(bool), String(String), Number(serde_json::Number), } -impl<'de> serde::de::Deserialize<'de> for ValuePrimative { +impl<'de> serde::de::Deserialize<'de> for ValuePrimitive { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = ValuePrimative; + type Value = ValuePrimitive; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { write!(formatter, "a JSON primative value") } @@ -161,37 +134,37 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Null) + Ok(ValuePrimitive::Null) } fn visit_none(self) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Null) + Ok(ValuePrimitive::Null) } fn visit_bool(self, v: bool) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Boolean(v)) + Ok(ValuePrimitive::Boolean(v)) } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::String(v.to_owned())) + Ok(ValuePrimitive::String(v.to_owned())) } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::String(v)) + Ok(ValuePrimitive::String(v)) } fn visit_f32(self, v: f32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number( + Ok(ValuePrimitive::Number( serde_json::Number::from_f64(v as f64).ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Float(v as f64), @@ -204,7 +177,7 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Number( + Ok(ValuePrimitive::Number( serde_json::Number::from_f64(v).ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Float(v), @@ -217,56 +190,56 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u16(self, v: u16) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u32(self, v: u32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u64(self, v: u64) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i8(self, v: i8) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i16(self, v: i16) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i32(self, v: i32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i64(self, v: i64) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } } deserializer.deserialize_any(Visitor) } } -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "kebab-case")] pub enum IoFormat { Json, @@ -297,7 +270,7 @@ impl std::fmt::Display for IoFormat { impl std::str::FromStr for IoFormat { type Err = Error; fn from_str(s: &str) -> Result { - serde_json::from_value(Value::String(s.to_owned())) + serde_json::from_value(serde_json::Value::String(s.to_owned())) .with_kind(crate::ErrorKind::Deserialization) } } @@ -425,36 +398,236 @@ impl IoFormat { } } -pub fn display_serializable(t: T, matches: &ArgMatches) { - let format = match matches.value_of("format").map(|f| f.parse()) { - Some(Ok(f)) => f, - Some(Err(_)) => { - eprintln!("unrecognized formatter"); - exit(1) - } - None => IoFormat::default(), - }; +pub fn display_serializable(format: IoFormat, result: T) { format - .to_writer(std::io::stdout(), &t) - .expect("Error serializing result to stdout") -} - -pub fn parse_stdin_deserializable Deserialize<'de>>( - stdin: &mut std::io::Stdin, - matches: &ArgMatches, -) -> Result { - let format = match matches.value_of("format").map(|f| f.parse()) { - Some(Ok(f)) => f, - Some(Err(_)) => { - eprintln!("unrecognized formatter"); - exit(1) + .to_writer(std::io::stdout(), &result) + .expect("Error serializing result to stdout"); + if format == IoFormat::JsonPretty { + println!() + } +} + +#[derive(Deserialize, Serialize)] +pub struct WithIoFormat { + pub format: Option, + #[serde(flatten)] + pub rest: T, +} +impl FromArgMatches for WithIoFormat { + fn from_arg_matches(matches: &ArgMatches) -> Result { + Ok(Self { + rest: T::from_arg_matches(matches)?, + format: matches.get_one("format").copied(), + }) + } + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> { + self.rest.update_from_arg_matches(matches)?; + self.format = matches.get_one("format").copied(); + Ok(()) + } +} +impl CommandFactory for WithIoFormat { + fn command() -> clap::Command { + let cmd = T::command(); + if !cmd.get_arguments().any(|a| a.get_id() == "format") { + cmd.arg( + clap::Arg::new("format") + .long("format") + .value_parser(|s: &str| s.parse::().map_err(|e| eyre!("{e}"))), + ) + } else { + cmd } - None => IoFormat::default(), - }; - format.from_reader(stdin) + } + fn command_for_update() -> clap::Command { + let cmd = T::command_for_update(); + if !cmd.get_arguments().any(|a| a.get_id() == "format") { + cmd.arg( + clap::Arg::new("format") + .long("format") + .value_parser(|s: &str| s.parse::().map_err(|e| eyre!("{e}"))), + ) + } else { + cmd + } + } } -#[derive(Debug, Clone, Copy)] +pub trait HandlerExtSerde: HandlerFor { + fn with_display_serializable(self) -> DisplaySerializable; +} +impl, C: Context> HandlerExtSerde for T { + fn with_display_serializable(self) -> DisplaySerializable { + DisplaySerializable(self) + } +} + +#[derive(Debug, Clone)] +pub struct DisplaySerializable(pub T); +impl HandlerTypes for DisplaySerializable { + type Params = WithIoFormat; + type InheritedParams = T::InheritedParams; + type Ok = T::Ok; + type Err = T::Err; +} +impl, C: Context> HandlerFor for DisplaySerializable { + fn handle_sync( + &self, + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgsFor, + ) -> Result { + self.0.handle_sync(HandlerArgs { + context, + parent_method, + method, + params: params.rest, + inherited_params, + raw_params, + }) + } + async fn handle_async( + &self, + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgsFor, + ) -> Result { + self.0 + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: params.rest, + inherited_params, + raw_params, + }) + .await + } + fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, imbl_value::Value> { + self.0.metadata(method) + } + fn method_from_dots(&self, method: &str) -> Option> { + self.0.method_from_dots(method) + } +} +impl PrintCliResult for DisplaySerializable +where + T::Ok: Serialize, +{ + fn print( + &self, + HandlerArgs { params, .. }: HandlerArgsFor, + result: Self::Ok, + ) -> Result<(), Self::Err> { + display_serializable(params.format.unwrap_or_default(), result); + Ok(()) + } +} +impl CliBindings for DisplaySerializable +where + Context: crate::Context, + Self: HandlerTypes, + Self::Params: CommandFactory + FromArgMatches + Serialize, + Self: PrintCliResult, +{ + fn cli_command(&self) -> clap::Command { + Self::Params::command() + } + fn cli_parse( + &self, + matches: &clap::ArgMatches, + ) -> Result<(VecDeque<&'static str>, patch_db::Value), clap::Error> { + Self::Params::from_arg_matches(matches).and_then(|a| { + Ok(( + VecDeque::new(), + imbl_value::to_value(&a) + .map_err(|e| clap::Error::raw(clap::error::ErrorKind::ValueValidation, e))?, + )) + }) + } + fn cli_display( + &self, + handle_args: HandlerArgsFor, + result: Self::Ok, + ) -> Result<(), Self::Err> { + self.print(handle_args, result) + } +} + +#[derive(Deserialize, Serialize, TS, Clone)] +pub struct StdinDeserializable(pub T); +impl Default for StdinDeserializable +where + T: Default, +{ + fn default() -> Self { + Self(T::default()) + } +} +impl FromArgMatches for StdinDeserializable +where + T: DeserializeOwned, +{ + fn from_arg_matches(matches: &ArgMatches) -> Result { + let format = matches + .get_one::("format") + .copied() + .unwrap_or_default(); + Ok(Self(format.from_reader(&mut std::io::stdin()).map_err( + |e| clap::Error::raw(clap::error::ErrorKind::ValueValidation, e), + )?)) + } + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> { + let format = matches + .get_one::("format") + .copied() + .unwrap_or_default(); + self.0 = format + .from_reader(&mut std::io::stdin()) + .map_err(|e| clap::Error::raw(clap::error::ErrorKind::ValueValidation, e))?; + Ok(()) + } +} +impl clap::Args for StdinDeserializable +where + T: DeserializeOwned, +{ + fn augment_args(cmd: clap::Command) -> clap::Command { + if !cmd.get_arguments().any(|a| a.get_id() == "format") { + cmd.arg( + clap::Arg::new("format") + .long("format") + .value_parser(|s: &str| s.parse::().map_err(|e| eyre!("{e}"))), + ) + } else { + cmd + } + } + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + if !cmd.get_arguments().any(|a| a.get_id() == "format") { + cmd.arg( + clap::Arg::new("format") + .long("format") + .value_parser(|s: &str| s.parse::().map_err(|e| eyre!("{e}"))), + ) + } else { + cmd + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, TS)] +#[ts(export, type = "string")] pub struct Duration(std::time::Duration); impl Deref for Duration { type Target = std::time::Duration; @@ -518,6 +691,12 @@ impl std::str::FromStr for Duration { })) } } +impl ValueParserFactory for Duration { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} impl std::fmt::Display for Duration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let nanos = self.as_nanos(); @@ -751,14 +930,56 @@ impl<'de, K: Deserialize<'de>, V: Deserialize<'de>> Deserialize<'de> for KeyVal< } } +#[derive(TS)] +#[ts(type = "string", concrete(T = Vec))] +pub struct Base16(pub T); +impl<'de, T: TryFrom>> Deserialize<'de> for Base16 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + hex::decode(&s) + .map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&s), + &"a valid hex string", + ) + })? + .try_into() + .map_err(|_| serde::de::Error::custom("invalid length")) + .map(Self) + } +} +impl> Serialize for Base16 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex::encode(self.0.as_ref())) + } +} +impl> std::fmt::Display for Base16 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + hex::encode(self.0.as_ref()).fmt(f) + } +} + +#[derive(TS)] +#[ts(type = "string", concrete(T = Vec))] pub struct Base32(pub T); +impl> std::fmt::Display for Base32 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + base32::encode(base32::Alphabet::Rfc4648 { padding: true }, self.0.as_ref()).fmt(f) + } +} impl<'de, T: TryFrom>> Deserialize<'de> for Base32 { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - base32::decode(base32::Alphabet::RFC4648 { padding: true }, &s) + base32::decode(base32::Alphabet::Rfc4648 { padding: true }, &s) .ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&s), @@ -775,25 +996,51 @@ impl> Serialize for Base32 { where S: Serializer, { - serializer.serialize_str(&base32::encode( - base32::Alphabet::RFC4648 { padding: true }, - self.0.as_ref(), - )) + serialize_display(self, serializer) } } +pub const BASE64: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] +#[ts(type = "string", concrete(T = Vec))] pub struct Base64(pub T); +impl> std::fmt::Display for Base64 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&BASE64.encode(self.0.as_ref())) + } +} +impl>> FromStr for Base64 { + type Err = Error; + fn from_str(s: &str) -> Result { + BASE64 + .decode(&s) + .with_kind(ErrorKind::Deserialization)? + .apply(TryFrom::try_from) + .map(Self) + .map_err(|_| { + Error::new( + eyre!("failed to create from buffer"), + ErrorKind::Deserialization, + ) + }) + } +} +impl>> ValueParserFactory for Base64 { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} impl<'de, T: TryFrom>> Deserialize<'de> for Base64 { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - base64::decode(&s) - .map_err(serde::de::Error::custom)? - .try_into() - .map_err(|_| serde::de::Error::custom("invalid length")) - .map(Self) + deserialize_from_str(deserializer) } } impl> Serialize for Base64 { @@ -801,7 +1048,13 @@ impl> Serialize for Base64 { where S: Serializer, { - serializer.serialize_str(&base64::encode(self.0.as_ref())) + serialize_display(self, serializer) + } +} +impl Deref for Base64 { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -843,3 +1096,309 @@ impl Serialize for Regex { serialize_display(&self.0, serializer) } } + +// TODO: make this not allocate +#[derive(Debug)] +pub struct NoOutput; +impl<'de> Deserialize<'de> for NoOutput { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let _ = Value::deserialize(deserializer); + Ok(NoOutput) + } +} + +pub fn apply_expr(input: jaq_core::Val, expr: &str) -> Result { + let (expr, errs) = jaq_core::parse::parse(expr, jaq_core::parse::main()); + + let Some(expr) = expr else { + return Err(Error::new( + eyre!("Failed to parse expression: {:?}", errs), + crate::ErrorKind::InvalidRequest, + )); + }; + + let mut errs = Vec::new(); + + let mut defs = jaq_core::Definitions::core(); + for def in jaq_std::std() { + defs.insert(def, &mut errs); + } + + let filter = defs.finish(expr, Vec::new(), &mut errs); + + if !errs.is_empty() { + return Err(Error::new( + eyre!("Failed to compile expression: {:?}", errs), + crate::ErrorKind::InvalidRequest, + )); + }; + + let inputs = jaq_core::RcIter::new(std::iter::empty()); + let mut res_iter = filter.run(jaq_core::Ctx::new([], &inputs), input); + + let Some(res) = res_iter + .next() + .transpose() + .map_err(|e| eyre!("{e}")) + .with_kind(crate::ErrorKind::Deserialization)? + else { + return Err(Error::new( + eyre!("expr returned no results"), + crate::ErrorKind::InvalidRequest, + )); + }; + + if res_iter.next().is_some() { + return Err(Error::new( + eyre!("expr returned too many results"), + crate::ErrorKind::InvalidRequest, + )); + } + + Ok(res) +} + +pub trait PemEncoding: Sized { + fn from_pem(pem: &str) -> Result; + fn to_pem(&self) -> Result; +} + +impl PemEncoding for X509 { + fn from_pem(pem: &str) -> Result { + Self::from_pem(pem.as_bytes()).map_err(E::custom) + } + fn to_pem(&self) -> Result { + String::from_utf8((&**self).to_pem().map_err(E::custom)?).map_err(E::custom) + } +} + +impl PemEncoding for PKey { + fn from_pem(pem: &str) -> Result { + Self::private_key_from_pem(pem.as_bytes()).map_err(E::custom) + } + fn to_pem(&self) -> Result { + String::from_utf8((&**self).private_key_to_pem_pkcs8().map_err(E::custom)?) + .map_err(E::custom) + } +} + +impl PemEncoding for ssh_key::PrivateKey { + fn from_pem(pem: &str) -> Result { + Self::from_openssh(pem.as_bytes()).map_err(E::custom) + } + fn to_pem(&self) -> Result { + self.to_openssh(ssh_key::LineEnding::LF) + .map_err(E::custom) + .map(|s| (&*s).clone()) + } +} + +impl PemEncoding for ed25519_dalek::VerifyingKey { + fn from_pem(pem: &str) -> Result { + use ed25519_dalek::pkcs8::DecodePublicKey; + Self::from_public_key_pem(pem).map_err(E::custom) + } + fn to_pem(&self) -> Result { + use ed25519_dalek::pkcs8::EncodePublicKey; + self.to_public_key_pem(pkcs8::LineEnding::LF) + .map_err(E::custom) + } +} + +impl PemEncoding for ed25519_dalek::SigningKey { + fn from_pem(pem: &str) -> Result { + use ed25519_dalek::pkcs8::DecodePrivateKey; + Self::from_pkcs8_pem(pem).map_err(E::custom) + } + fn to_pem(&self) -> Result { + use ed25519_dalek::pkcs8::EncodePrivateKey; + self.to_pkcs8_pem(pkcs8::LineEnding::LF) + .map_err(E::custom) + .map(|s| s.as_str().to_owned()) + } +} + +#[derive(Clone, Debug)] +pub struct Pkcs8Doc { + pub tag: String, + pub document: pkcs8::Document, +} + +impl PemEncoding for Pkcs8Doc { + fn from_pem(pem: &str) -> Result { + let (tag, document) = pkcs8::Document::from_pem(pem).map_err(E::custom)?; + Ok(Pkcs8Doc { + tag: tag.into(), + document, + }) + } + fn to_pem(&self) -> Result { + der::pem::encode_string( + &self.tag, + pkcs8::LineEnding::default(), + self.document.as_bytes(), + ) + .map_err(E::custom) + } +} + +pub mod pem { + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::util::serde::PemEncoding; + + pub fn serialize( + value: &T, + serializer: S, + ) -> Result { + serializer.serialize_str(&value.to_pem()?) + } + + pub fn deserialize<'de, T: PemEncoding, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let pem = String::deserialize(deserializer)?; + Ok(T::from_pem(&pem)?) + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] +#[ts(type = "string", concrete(T = ed25519_dalek::VerifyingKey))] +pub struct Pem(#[serde(with = "pem")] pub T); +impl Pem { + pub fn new(value: T) -> Self { + Pem(value) + } + pub fn new_ref(value: &T) -> &Self { + unsafe { std::mem::transmute(value) } + } + pub fn new_mut(value: &mut T) -> &mut Self { + unsafe { std::mem::transmute(value) } + } +} +impl Deref for Pem { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::fmt::Display for Pem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_pem::() + .map_err(|_| std::fmt::Error::default())? + .fmt(f) + } +} +impl FromStr for Pem { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(Self( + T::from_pem::(s).with_kind(ErrorKind::Pem)?, + )) + } +} +impl ValueParserFactory for Pem { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, TS)] +#[ts(export, type = "string | number[]")] +pub struct MaybeUtf8String(pub Vec); +impl std::fmt::Debug for MaybeUtf8String { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Ok(s) = std::str::from_utf8(&self.0) { + s.fmt(f) + } else { + self.0.fmt(f) + } + } +} +impl<'de> Deserialize<'de> for MaybeUtf8String { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Vec; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "a string or byte array") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(v.as_bytes().to_owned()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + Ok(v.into_bytes()) + } + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(v.to_owned()) + } + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + Ok(v) + } + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(Vec::new()) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + std::iter::repeat_with(|| seq.next_element::().transpose()) + .take_while(|a| a.is_some()) + .flatten() + .collect::, _>>() + } + } + deserializer.deserialize_any(Visitor).map(Self) + } +} +impl Serialize for MaybeUtf8String { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if let Ok(s) = std::str::from_utf8(&self.0) { + serializer.serialize_str(s) + } else { + serializer.serialize_bytes(&self.0) + } + } +} + +pub fn is_partial_of(partial: &Value, full: &Value) -> bool { + match (partial, full) { + (Value::Object(partial), Value::Object(full)) => partial.iter().all(|(k, v)| { + if let Some(v_full) = full.get(k) { + is_partial_of(v, v_full) + } else { + false + } + }), + (Value::Array(partial), Value::Array(full)) => partial + .iter() + .all(|v| full.iter().any(|v_full| is_partial_of(v, v_full))), + (_, _) => partial == full, + } +} diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs new file mode 100644 index 000000000..d6099cb5b --- /dev/null +++ b/core/startos/src/util/sync.rs @@ -0,0 +1,162 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Poll, Waker}; + +#[derive(Debug, Default)] +pub struct SyncMutex(std::sync::Mutex); +impl SyncMutex { + pub fn new(t: T) -> Self { + Self(std::sync::Mutex::new(t)) + } + pub fn mutate U, U>(&self, f: F) -> U { + f(&mut *self.0.lock().unwrap()) + } + pub fn peek U, U>(&self, f: F) -> U { + f(&*self.0.lock().unwrap()) + } +} + +struct WatchShared { + version: u64, + data: T, + wakers: Vec, +} +impl WatchShared { + fn modified(&mut self) { + self.version += 1; + for waker in self.wakers.drain(..) { + waker.wake(); + } + } +} + +#[pin_project::pin_project] +pub struct Watch { + shared: Arc>>, + version: u64, +} +impl Clone for Watch { + fn clone(&self) -> Self { + Self { + shared: self.shared.clone(), + version: self.version, + } + } +} +impl Watch { + pub fn new(init: T) -> Self { + Self { + shared: Arc::new(SyncMutex::new(WatchShared { + version: 1, + data: init, + wakers: Vec::new(), + })), + version: 0, + } + } + pub fn clone_unseen(&self) -> Self { + Self { + shared: self.shared.clone(), + version: 0, + } + } + pub fn poll_changed(&mut self, cx: &mut std::task::Context<'_>) -> Poll<()> { + self.shared.mutate(|shared| { + if shared.version != self.version { + self.version = shared.version; + Poll::Ready(()) + } else { + let waker = cx.waker(); + if !shared.wakers.iter().any(|w| w.will_wake(waker)) { + shared.wakers.push(waker.clone()); + } + Poll::Pending + } + }) + } + pub async fn changed(&mut self) { + futures::future::poll_fn(|cx| self.poll_changed(cx)).await + } + pub async fn wait_for bool>(&mut self, mut f: F) { + loop { + if self.peek(&mut f) { + break; + } + self.changed().await; + } + } + pub fn send_if_modified bool>(&self, modify: F) -> bool { + self.shared.mutate(|shared| { + let changed = modify(&mut shared.data); + if changed { + shared.modified(); + } + changed + }) + } + pub fn send_modify U>(&self, modify: F) -> U { + self.shared.mutate(|shared| { + let res = modify(&mut shared.data); + shared.modified(); + res + }) + } + pub fn send_replace(&self, new: T) -> T { + self.send_modify(|a| std::mem::replace(a, new)) + } + pub fn send(&self, new: T) { + self.send_replace(new); + } + pub fn mark_changed(&self) { + self.shared.mutate(|shared| shared.modified()) + } + pub fn mark_unseen(&mut self) { + self.version = 0; + } + pub fn mark_seen(&mut self) { + self.shared.peek(|shared| { + self.version = shared.version; + }) + } + pub fn peek U>(&self, f: F) -> U { + self.shared.peek(|shared| f(&shared.data)) + } + pub fn peek_and_mark_seen U>(&mut self, f: F) -> U { + self.shared.peek(|shared| { + self.version = shared.version; + f(&shared.data) + }) + } + pub fn peek_mut U>(&self, f: F) -> U { + self.shared.mutate(|shared| f(&mut shared.data)) + } +} +impl Watch { + pub fn read(&self) -> T { + self.peek(|a| a.clone()) + } +} +impl futures::Stream for Watch { + type Item = T; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + this.shared.mutate(|shared| { + if shared.version != *this.version { + *this.version = shared.version; + Poll::Ready(Some(shared.data.clone())) + } else { + let waker = cx.waker(); + if !shared.wakers.iter().any(|w| w.will_wake(waker)) { + shared.wakers.push(waker.clone()); + } + Poll::Pending + } + }) + } + fn size_hint(&self) -> (usize, Option) { + (1, None) + } +} diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 4c6f157a5..2ff43fec2 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -1,148 +1,462 @@ +use std::any::Any; use std::cmp::Ordering; +use std::panic::{RefUnwindSafe, UnwindSafe}; -use async_trait::async_trait; use color_eyre::eyre::eyre; -use rpc_toolkit::command; -use sqlx::PgPool; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use imbl::Vector; +use imbl_value::{to_value, InternedString}; +use patch_db::json_ptr::ROOT; +use crate::context::RpcContext; +use crate::db::model::Database; use crate::prelude::*; +use crate::progress::PhaseProgressTrackerHandle; use crate::Error; -mod v0_3_4; -mod v0_3_4_1; -mod v0_3_4_2; -mod v0_3_4_3; -mod v0_3_4_4; mod v0_3_5; mod v0_3_5_1; +mod v0_3_5_2; +mod v0_3_6_alpha_0; +mod v0_3_6_alpha_1; +mod v0_3_6_alpha_2; +mod v0_3_6_alpha_3; +mod v0_3_6_alpha_4; +mod v0_3_6_alpha_5; +mod v0_3_6_alpha_6; +mod v0_3_6_alpha_7; +mod v0_3_6_alpha_8; +mod v0_3_6_alpha_9; -pub type Current = v0_3_5_1::Version; +mod v0_3_6_alpha_10; +mod v0_3_6_alpha_11; +mod v0_3_6_alpha_12; +mod v0_3_6_alpha_13; + +pub type Current = v0_3_6_alpha_13::Version; // VERSION_BUMP + +impl Current { + #[instrument(skip(self, db))] + pub async fn pre_init(self, db: &PatchDb) -> Result<(), Error> { + let from = from_value::( + version_accessor(&mut db.dump(&ROOT).await.value) + .or_not_found("`version` in db")? + .clone(), + )? + .as_version_t()?; + match from.semver().cmp(&self.semver()) { + Ordering::Greater => { + db.apply_function(|mut db| { + rollback_to_unchecked(&from, &self, &mut db)?; + Ok::<_, Error>((db, ())) + }) + .await?; + } + Ordering::Less => { + let pre_ups = PreUps::load(&from, &self).await?; + db.apply_function(|mut db| { + migrate_from_unchecked(&from, &self, pre_ups, &mut db)?; + Ok::<_, Error>((to_value(&from_value::(db.clone())?)?, ())) + }) + .await?; + } + Ordering::Equal => (), + } + Ok(()) + } +} + +pub async fn post_init( + ctx: &RpcContext, + mut progress: PhaseProgressTrackerHandle, +) -> Result<(), Error> { + let mut peek = ctx.db.peek().await; + let todos = peek + .as_public() + .as_server_info() + .as_post_init_migration_todos() + .de()?; + if !todos.is_empty() { + progress.set_total(todos.len() as u64); + while let Some(version) = { + peek = ctx.db.peek().await; + peek.as_public() + .as_server_info() + .as_post_init_migration_todos() + .de()? + .first() + .cloned() + .map(Version::from_exver_version) + .as_ref() + .map(Version::as_version_t) + .transpose()? + } { + version.0.post_up(ctx).await?; + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_post_init_migration_todos_mut() + .mutate(|m| Ok(m.remove(&version.0.semver()))) + }) + .await?; + progress += 1; + } + } + progress.complete(); + Ok(()) +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] +#[allow(non_camel_case_types)] enum Version { - V0_3_4(Wrapper), - V0_3_4_1(Wrapper), - V0_3_4_2(Wrapper), - V0_3_4_3(Wrapper), - V0_3_4_4(Wrapper), + LT0_3_5(LTWrapper), V0_3_5(Wrapper), V0_3_5_1(Wrapper), - Other(emver::Version), + V0_3_5_2(Wrapper), + V0_3_6_alpha_0(Wrapper), + V0_3_6_alpha_1(Wrapper), + V0_3_6_alpha_2(Wrapper), + V0_3_6_alpha_3(Wrapper), + V0_3_6_alpha_4(Wrapper), + V0_3_6_alpha_5(Wrapper), + V0_3_6_alpha_6(Wrapper), + V0_3_6_alpha_7(Wrapper), + V0_3_6_alpha_8(Wrapper), + V0_3_6_alpha_9(Wrapper), + V0_3_6_alpha_10(Wrapper), + V0_3_6_alpha_11(Wrapper), + V0_3_6_alpha_12(Wrapper), + V0_3_6_alpha_13(Wrapper), // VERSION_BUMP + Other(exver::Version), } impl Version { - fn from_util_version(version: crate::util::Version) -> Self { + fn from_exver_version(version: exver::Version) -> Self { serde_json::to_value(version.clone()) .and_then(serde_json::from_value) .unwrap_or_else(|_e| { tracing::warn!("Can't deserialize: {:?} and falling back to other", version); - Version::Other(version.into_version()) + Version::Other(version) }) } + fn as_version_t(&self) -> Result { + Ok(match self { + Self::LT0_3_5(_) => { + return Err(Error::new( + eyre!("cannot migrate from versions before 0.3.5"), + ErrorKind::MigrationFailed, + )) + } + Self::V0_3_5(v) => DynVersion(Box::new(v.0)), + Self::V0_3_5_1(v) => DynVersion(Box::new(v.0)), + Self::V0_3_5_2(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_0(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_1(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_2(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_3(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_4(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_5(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_6(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_7(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_8(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_9(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_10(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_11(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_12(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_13(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::Other(v) => { + return Err(Error::new( + eyre!("unknown version {v}"), + ErrorKind::MigrationFailed, + )) + } + }) + } #[cfg(test)] - fn as_sem_ver(&self) -> emver::Version { + fn as_exver(&self) -> exver::Version { match self { - Version::V0_3_4(Wrapper(x)) => x.semver(), - Version::V0_3_4_1(Wrapper(x)) => x.semver(), - Version::V0_3_4_2(Wrapper(x)) => x.semver(), - Version::V0_3_4_3(Wrapper(x)) => x.semver(), - Version::V0_3_4_4(Wrapper(x)) => x.semver(), + Version::LT0_3_5(LTWrapper(_, x)) => x.clone(), Version::V0_3_5(Wrapper(x)) => x.semver(), Version::V0_3_5_1(Wrapper(x)) => x.semver(), + Version::V0_3_5_2(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_1(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_2(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_3(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_4(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_5(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_6(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_7(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_8(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_9(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_10(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_11(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_12(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_13(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } } -#[async_trait] -pub trait VersionT -where - Self: Sized + Send + Sync, -{ - type Previous: VersionT; - fn new() -> Self; - fn semver(&self) -> emver::Version; - fn compat(&self) -> &'static emver::VersionRange; - async fn up(&self, db: PatchDb, secrets: &PgPool) -> Result<(), Error>; - async fn down(&self, db: PatchDb, secrets: &PgPool) -> Result<(), Error>; - async fn commit(&self, db: PatchDb) -> Result<(), Error> { - let semver = self.semver().into(); - let compat = self.compat().clone(); - db.mutate(|d| { - d.as_server_info_mut().as_version_mut().ser(&semver)?; - d.as_server_info_mut() - .as_eos_version_compat_mut() - .ser(&compat)?; - Ok(()) - }) - .await?; - Ok(()) +fn version_accessor(db: &mut Value) -> Option<&mut Value> { + if db.get("public").is_some() { + db.get_mut("public")? + .get_mut("serverInfo")? + .get_mut("version") + } else { + db.get_mut("server-info")?.get_mut("version") } - async fn migrate_to( - &self, - version: &V, - db: PatchDb, - secrets: &PgPool, - ) -> Result<(), Error> { - match self.semver().cmp(&version.semver()) { - Ordering::Greater => self.rollback_to_unchecked(version, db, secrets).await, - Ordering::Less => version.migrate_from_unchecked(self, db, secrets).await, - Ordering::Equal => Ok(()), +} + +fn version_compat_accessor(db: &mut Value) -> Option<&mut Value> { + if db.get("public").is_some() { + let server_info = db.get_mut("public")?.get_mut("serverInfo")?; + if server_info.get("packageVersionCompat").is_some() { + server_info.get_mut("packageVersionCompat") + } else { + if let Some(prev) = server_info.get("eosVersionCompat").cloned() { + server_info + .as_object_mut()? + .insert("packageVersionCompat".into(), prev); + } else if let Some(prev) = server_info.get("versionCompat").cloned() { + server_info + .as_object_mut()? + .insert("packageVersionCompat".into(), prev); + } + server_info.get_mut("packageVersionCompat") } + } else { + db.get_mut("server-info")?.get_mut("eos-version-compat") } - async fn migrate_from_unchecked( - &self, - version: &V, - db: PatchDb, - secrets: &PgPool, - ) -> Result<(), Error> { - let previous = Self::Previous::new(); - if version.semver() < previous.semver() { - previous - .migrate_from_unchecked(version, db.clone(), secrets) - .await?; - } else if version.semver() > previous.semver() { +} + +fn post_init_migration_todos_accessor(db: &mut Value) -> Option<&mut Value> { + let server_info = if db.get("public").is_some() { + db.get_mut("public")?.get_mut("serverInfo")? + } else { + db.get_mut("server-info")? + }; + if server_info.get("postInitMigrationTodos").is_none() { + server_info + .as_object_mut()? + .insert("postInitMigrationTodos".into(), Value::Array(Vector::new())); + } + server_info.get_mut("postInitMigrationTodos") +} + +struct PreUps { + prev: Option>, + value: Box, +} +impl PreUps { + #[instrument(skip(from, to))] + fn load<'a, VFrom: DynVersionT + ?Sized, VTo: DynVersionT + ?Sized>( + from: &'a VFrom, + to: &'a VTo, + ) -> BoxFuture<'a, Result> { + async { + let previous = to.previous(); + let prev = match from.semver().cmp(&previous.semver()) { + Ordering::Less => Some(Box::new(PreUps::load(from, &previous).await?)), + Ordering::Greater => { + return Err(Error::new( + eyre!( + "NO PATH FROM {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", + from.semver() + ), + crate::ErrorKind::MigrationFailed, + )) + } + Ordering::Equal => None, + }; + Ok(Self { + prev, + value: to.pre_up().await?, + }) + } + .boxed() + } +} + +fn migrate_from_unchecked( + from: &VFrom, + to: &VTo, + pre_ups: PreUps, + db: &mut Value, +) -> Result<(), Error> { + let previous = to.previous(); + match pre_ups.prev { + Some(prev) if from.semver() < previous.semver() => { + migrate_from_unchecked(from, &previous, *prev, db)? + } + _ if from.semver() > previous.semver() => { return Err(Error::new( eyre!( "NO PATH FROM {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", - version.semver() + from.semver() ), crate::ErrorKind::MigrationFailed, )); } - tracing::info!("{} -> {}", previous.semver(), self.semver(),); - self.up(db.clone(), secrets).await?; - self.commit(db).await?; + _ => (), + }; + to.up(db, pre_ups.value)?; + to.commit(db)?; + Ok(()) +} + +fn rollback_to_unchecked( + from: &VFrom, + to: &VTo, + db: &mut Value, +) -> Result<(), Error> { + let previous = from.previous(); + from.down(db)?; + previous.commit(db)?; + if to.semver() < previous.semver() { + rollback_to_unchecked(&previous, to, db)? + } else if to.semver() > previous.semver() { + return Err(Error::new( + eyre!( + "NO PATH TO {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", + to.semver() + ), + crate::ErrorKind::MigrationFailed, + )); + } + Ok(()) +} + +pub trait VersionT +where + Self: Default + Copy + Sized + RefUnwindSafe + Send + Sync + 'static, +{ + type Previous: VersionT; + type PreUpRes: Send + UnwindSafe; + fn semver(self) -> exver::Version; + fn compat(self) -> &'static exver::VersionRange; + /// MUST NOT change system state. Intended for async I/O reads + fn pre_up(self) -> impl Future> + Send + 'static; + fn up(self, db: &mut Value, input: Self::PreUpRes) -> Result<(), Error> { Ok(()) } - async fn rollback_to_unchecked( - &self, - version: &V, - db: PatchDb, - secrets: &PgPool, - ) -> Result<(), Error> { - let previous = Self::Previous::new(); - tracing::info!("{} -> {}", self.semver(), previous.semver(),); - self.down(db.clone(), secrets).await?; - previous.commit(db.clone()).await?; - if version.semver() < previous.semver() { - previous.rollback_to_unchecked(version, db, secrets).await?; - } else if version.semver() > previous.semver() { - return Err(Error::new( - eyre!( - "NO PATH TO {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", - version.semver() - ), - crate::ErrorKind::MigrationFailed, - )); - } + /// MUST be idempotent, and is run after *all* db migrations + fn post_up<'a>( + self, + ctx: &'a RpcContext, + ) -> impl Future> + Send + 'a { + async { Ok(()) } + } + fn down(self, db: &mut Value) -> Result<(), Error> { + Err(Error::new( + eyre!("downgrades prohibited"), + ErrorKind::InvalidRequest, + )) + } + fn commit(self, db: &mut Value) -> Result<(), Error> { + *version_accessor(db).or_not_found("`version` in db")? = to_value(&self.semver())?; + *version_compat_accessor(db).or_not_found("`versionCompat` in db")? = + to_value(self.compat())?; + post_init_migration_todos_accessor(db) + .or_not_found("`serverInfo` in db")? + .as_array_mut() + .ok_or_else(|| { + Error::new( + eyre!("postInitMigrationTodos is not an array"), + ErrorKind::Database, + ) + })? + .push_back(to_value(&self.semver())?); Ok(()) } } + +struct DynVersion(Box); +unsafe impl Send for DynVersion {} + +trait DynVersionT: RefUnwindSafe + Send + Sync { + fn previous(&self) -> DynVersion; + fn semver(&self) -> exver::Version; + fn compat(&self) -> &'static exver::VersionRange; + fn pre_up(&self) -> BoxFuture<'static, Result, Error>>; + fn up(&self, db: &mut Value, input: Box) -> Result<(), Error>; + fn post_up<'a>(&self, ctx: &'a RpcContext) -> BoxFuture<'a, Result<(), Error>>; + fn down(&self, db: &mut Value) -> Result<(), Error>; + fn commit(&self, db: &mut Value) -> Result<(), Error>; +} +impl DynVersionT for T +where + T: VersionT, +{ + fn previous(&self) -> DynVersion { + DynVersion(Box::new(::Previous::default())) + } + fn semver(&self) -> exver::Version { + VersionT::semver(*self) + } + fn compat(&self) -> &'static exver::VersionRange { + VersionT::compat(*self) + } + fn pre_up(&self) -> BoxFuture<'static, Result, Error>> { + let v = *self; + async move { Ok(Box::new(VersionT::pre_up(v).await?) as Box) } + .boxed() + } + fn up(&self, db: &mut Value, input: Box) -> Result<(), Error> { + VersionT::up( + *self, + db, + *input.downcast().map_err(|_| { + Error::new( + eyre!("pre_up returned unexpected type"), + ErrorKind::Incoherent, + ) + })?, + ) + } + fn post_up<'a>(&self, ctx: &'a RpcContext) -> BoxFuture<'a, Result<(), Error>> { + VersionT::post_up(*self, ctx).boxed() + } + fn down(&self, db: &mut Value) -> Result<(), Error> { + VersionT::down(*self, db) + } + fn commit(&self, db: &mut Value) -> Result<(), Error> { + VersionT::commit(*self, db) + } +} +impl DynVersionT for DynVersion { + fn previous(&self) -> DynVersion { + self.0.previous() + } + fn semver(&self) -> exver::Version { + self.0.semver() + } + fn compat(&self) -> &'static exver::VersionRange { + self.0.compat() + } + fn pre_up(&self) -> BoxFuture<'static, Result, Error>> { + self.0.pre_up() + } + fn up(&self, db: &mut Value, input: Box) -> Result<(), Error> { + self.0.up(db, input) + } + fn post_up<'a>(&self, ctx: &'a RpcContext) -> BoxFuture<'a, Result<(), Error>> { + self.0.post_up(ctx) + } + fn down(&self, db: &mut Value) -> Result<(), Error> { + self.0.down(db) + } + fn commit(&self, db: &mut Value) -> Result<(), Error> { + self.0.commit(db) + } +} + #[derive(Debug, Clone)] -struct Wrapper(T); -impl serde::Serialize for Wrapper +struct LTWrapper(T, exver::Version); +impl serde::Serialize for LTWrapper where T: VersionT, { @@ -150,48 +464,51 @@ where self.0.semver().serialize(serializer) } } -impl<'de, T> serde::Deserialize<'de> for Wrapper +impl<'de, T> serde::Deserialize<'de> for LTWrapper where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = crate::util::Version::deserialize(deserializer)?; - let version = T::new(); - if *v == version.semver() { - Ok(Wrapper(version)) + let v = exver::Version::deserialize(deserializer)?; + let version = T::default(); + if v < version.semver() { + Ok(Self(version, v)) } else { Err(serde::de::Error::custom("Mismatched Version")) } } } -pub async fn init(db: &PatchDb, secrets: &PgPool) -> Result<(), Error> { - let version = Version::from_util_version(db.peek().await.as_server_info().as_version().de()?); - - match version { - Version::V0_3_4(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_4_1(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_4_2(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_4_3(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_4_4(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_5(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), db.clone(), secrets).await?, - Version::Other(_) => { - return Err(Error::new( - eyre!("Cannot downgrade"), - crate::ErrorKind::InvalidRequest, - )) +#[derive(Debug, Clone)] +struct Wrapper(T); +impl serde::Serialize for Wrapper +where + T: VersionT, +{ + fn serialize(&self, serializer: S) -> Result { + self.0.semver().serialize(serializer) + } +} +impl<'de, T> serde::Deserialize<'de> for Wrapper +where + T: VersionT, +{ + fn deserialize>(deserializer: D) -> Result { + let v = exver::Version::deserialize(deserializer)?; + let version = T::default(); + if v == version.semver() { + Ok(Wrapper(version)) + } else { + Err(serde::de::Error::custom("Mismatched Version")) } } - Ok(()) } pub const COMMIT_HASH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../GIT_HASH.txt")); -#[command(rename = "git-info", local, metadata(authenticated = false))] -pub fn git_info() -> Result<&'static str, Error> { - Ok(COMMIT_HASH) +pub fn git_info() -> Result { + Ok(InternedString::intern(COMMIT_HASH)) } #[cfg(test)] @@ -200,36 +517,50 @@ mod tests { use super::*; - fn em_version() -> impl Strategy { - any::<(usize, usize, usize, usize)>().prop_map(|(major, minor, patch, super_minor)| { - emver::Version::new(major, minor, patch, super_minor) + fn em_version() -> impl Strategy { + any::<(usize, usize, usize, bool)>().prop_map(|(major, minor, patch, alpha)| { + if alpha { + exver::Version::new( + [0, major, minor] + .into_iter() + .chain(Some(patch).filter(|n| *n != 0)), + [], + ) + } else { + exver::Version::new([major, minor, patch], []) + } }) } fn versions() -> impl Strategy { prop_oneof![ - Just(Version::V0_3_4(Wrapper(v0_3_4::Version::new()))), - Just(Version::V0_3_4_1(Wrapper(v0_3_4_1::Version::new()))), - Just(Version::V0_3_4_2(Wrapper(v0_3_4_2::Version::new()))), - Just(Version::V0_3_4_3(Wrapper(v0_3_4_3::Version::new()))), - Just(Version::V0_3_4_4(Wrapper(v0_3_4_4::Version::new()))), - Just(Version::V0_3_5(Wrapper(v0_3_5::Version::new()))), - Just(Version::V0_3_5_1(Wrapper(v0_3_5_1::Version::new()))), + Just(Version::V0_3_5(Wrapper(v0_3_5::Version::default()))), + Just(Version::V0_3_5_1(Wrapper(v0_3_5_1::Version::default()))), + Just(Version::V0_3_5_2(Wrapper(v0_3_5_2::Version::default()))), + Just(Version::V0_3_6_alpha_0(Wrapper( + v0_3_6_alpha_0::Version::default() + ))), + Just(Version::V0_3_6_alpha_1(Wrapper( + v0_3_6_alpha_1::Version::default() + ))), + Just(Version::V0_3_6_alpha_2(Wrapper( + v0_3_6_alpha_2::Version::default() + ))), em_version().prop_map(Version::Other), ] } proptest! { #[test] - fn emversion_isomorphic_version(original in em_version()) { - let version = Version::from_util_version(original.clone().into()); - let back = version.as_sem_ver(); + fn exversion_isomorphic_version(original in em_version()) { + let version = Version::from_exver_version(original.clone().into()); + let back = version.as_exver(); prop_assert_eq!(original, back, "All versions should round trip"); } #[test] fn version_isomorphic_em_version(version in versions()) { - let sem_ver = version.as_sem_ver(); - let back = Version::from_util_version(sem_ver.into()); + let sem_ver = version.as_exver(); + let back = Version::from_exver_version(sem_ver.into()); prop_assert_eq!(format!("{:?}",version), format!("{:?}", back), "All versions should round trip"); } } diff --git a/core/startos/src/version/update_details/v0_3_6.md b/core/startos/src/version/update_details/v0_3_6.md new file mode 100644 index 000000000..177845f08 --- /dev/null +++ b/core/startos/src/version/update_details/v0_3_6.md @@ -0,0 +1,84 @@ +# StartOS v0.3.6 + +## Warning + +Previous backups are incompatible with v0.3.6. It is strongly recommended that you (1) immediately update all services, then (2) create a fresh backup. See the [backups](#improved-backups) section below for more details. + +## Summary + +Servers are not toys. They are a critical component of the computing paradigm, and their failure can be catastrophic, resulting in downtime or loss of data. From the beginning, Start9 has taken a "security and reliability first" approach to the development of StartOS, favoring soundness over speed and prioritizing essential features such as encrypted network connections, simple backups, and a reliable container runtime over nice-to-haves like custom theming and more apps. + +Start9 is paving new ground with StartOS, trying to achieve what most developers and IT professionals thought impossible; namely, giving a normal person the same independent control over their data and communications as an experienced Linux sysadmin. + +A consequence of our principled approach to development, combined with the difficulty of our endeavor, is that (1) mistakes will be made and (2) they must be corrected. That means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to nuke everything and start over from scratch. We did this in 2020 with StartOS v0.2.0, again in 2022 with StartOS v0.3.0, and now in 2024 with StartOS v0.3.6. + +StartOS v0.3.6 is a complete rewrite of the OS internals (everything you don't see). Almost nothing survived. After nearly five years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation, and that no additional rewrites will be necessary for StartOS to deliver on its promise. + +## Changelog + +- [Switch to lxc-based container runtime](#lxc) +- [Update s9pk archive format](#s9pk-archive-format) +- [Improve Actions](#actions) +- [Use squashfs images for OS updates](#squashfs-updates) +- [Introduce Typescript package API and SDK](#typescript-sdk) +- [Remove Postgresql](#remove-postgressql) +- [Implement detailed progress reporting](#progress-reporting) +- [Improve registry protocol](#registry-protocol) +- [Replace unique .local URLs with unique ports](#lan-port-forwarding) +- [Use start-fs Fuse module for improved backups](#improved-backups) +- [Switch to Exver for versioning](#exver) +- [Support clearnet hosting via start-cli](#clearnet) + +### LXC + +StartOS now uses a nested container paradigm based on LXC for the outer container, and using linux namespaces for the inner lite containers. This replaces both Docker and Podman. + +### S9PK archive format + +The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk. + +### Actions + +Actions take arbitrary form input and return arbitrary responses, thus satisfying the needs of both Config and Properties, which have been removed. The new actions API gives packages developers the ability to break up Config and Properties into smaller, more specific formats, or to exclude them entirely without polluting the UI. Improved form design and new input types round out the actions experience. + +### Squashfs updates + +StartOS now uses squashfs images to represent OS updates. This allows for better update verification, and improved reliability over rsync updates. + +### Typescript SDK + +Package developers can now take advantage of StartOS APIs using the new start-sdk, available in Typescript. A barebones StartOS package (s9pk) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences with their service. + +### Remove PostgresSQL + +StartOS itself has miniscule data persistence needs. PostgresSQL was overkill and has been removed in favor of lightweight PatchDB. + +### Progress reporting + +A new progress reporting API enabled package developers to create unique phases and provide real-time progress reporting for actions such as installing, updating, or backing up a service. + +### Registry protocol + +The new registry protocol bifurcates package indexing (listing/validating) and package hosting (downloading). Registries are now simple indexes of packages that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the currated list of packages comes from Start9. But when someone installs a listed service, the package binary is being downloaded from Github. The registry also valides the binary. This makes it much easier to host a custom registry, since it is just a currated list of services tat reference package binaries hosted on Github or elsewhere. + +### LAN port forwarding + +Perhaps the biggest complaint with prior version of StartOS was use of unique .local URLs for service interfaces. This has been corrected. Service interfaces are now available on unique ports, allowing for non-http traffic on the LAN as well as remote access via VPN. + +### Improved Backups + +The new start-fs fuse module unifies file system expectations for various platforms, enabling more reliable backups. The new system also defaults to using rsync differential backups instead of incremental backups, which is faster and saves on disk space by also deleting from the backup files that were deleted from the server. + +### Exver + +StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:28.0.:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 27.0". + +### Clearnet + +It is now possible, and quite easy, to expose service interfaces to the public Internet on a standard domain using start-cli. In addition to choosing which service interfaces to expose on which domains/subdomains, users have two options: + +1. Open ports on their router. This option is free and easy to accomplish with most routers. The drawback is that the user's home IP address is revealed to anyone accessing the exposes resources. For example, hosting a blog in this way would reveal your home IP address, and therefor your approximate location on Earth, to your readers. + +2. Use a Wireguard VPN to proxy web traffic. This option requires the user to provision a $5-$10/month remote VPS and perform a few, simple commands. The result is the successful obfuscation of the users home IP address. + +The CLI-driven clearnet functionality will be expanded upon and moved into the main StartOS UI in a future release. diff --git a/core/startos/src/version/v0_3_4.rs b/core/startos/src/version/v0_3_4.rs deleted file mode 100644 index e33dcb931..000000000 --- a/core/startos/src/version/v0_3_4.rs +++ /dev/null @@ -1,135 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; -use itertools::Itertools; -use openssl::hash::MessageDigest; -use serde_json::json; -use ssh_key::public::Ed25519PublicKey; - -use super::*; -use crate::account::AccountInfo; -use crate::hostname::{sync_hostname, Hostname}; -use crate::prelude::*; - -const V0_3_4: emver::Version = emver::Version::new(0, 3, 4, 0); - -lazy_static::lazy_static! { - pub static ref V0_3_0_COMPAT: VersionRange = VersionRange::Conj( - Box::new(VersionRange::Anchor( - emver::GTE, - emver::Version::new(0, 3, 0, 0), - )), - Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())), - ); -} - -const COMMUNITY_URL: &str = "https://community-registry.start9.com/"; -const MAIN_REGISTRY: &str = "https://registry.start9.com/"; -const COMMUNITY_SERVICES: &[&str] = &[ - "ipfs", - "agora", - "lightning-jet", - "balanceofsatoshis", - "mastodon", - "lndg", - "robosats", - "thunderhub", - "syncthing", - "sphinx-relay", -]; - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = Self; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_3_4 - } - fn compat(&self) -> &'static VersionRange { - &*V0_3_0_COMPAT - } - async fn up(&self, db: PatchDb, secrets: &PgPool) -> Result<(), Error> { - let mut account = AccountInfo::load(secrets).await?; - let account = db - .mutate(|d| { - d.as_server_info_mut().as_pubkey_mut().ser( - &ssh_key::PublicKey::from(Ed25519PublicKey::from(&account.key.ssh_key())) - .to_openssh()?, - )?; - d.as_server_info_mut().as_ca_fingerprint_mut().ser( - &account - .root_ca_cert - .digest(MessageDigest::sha256()) - .unwrap() - .iter() - .map(|x| format!("{x:X}")) - .join(":"), - )?; - let server_info = d.as_server_info(); - account.hostname = server_info.as_hostname().de().map(Hostname)?; - account.server_id = server_info.as_id().de()?; - - Ok(account) - }) - .await?; - account.save(secrets).await?; - sync_hostname(&account.hostname).await?; - - let parsed_url = Some(COMMUNITY_URL.parse().unwrap()); - db.mutate(|d| { - let mut ui = d.as_ui().de()?; - use imbl_value::json; - ui["marketplace"]["known-hosts"][COMMUNITY_URL] = json!({}); - ui["marketplace"]["known-hosts"][MAIN_REGISTRY] = json!({}); - for package_id in d.as_package_data().keys()? { - if !COMMUNITY_SERVICES.contains(&&*package_id.to_string()) { - continue; - } - d.as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? - .as_marketplace_url_mut() - .ser(&parsed_url)?; - } - ui["theme"] = json!("Dark".to_string()); - ui["widgets"] = json!([]); - - d.as_ui_mut().ser(&ui) - }) - .await - } - async fn down(&self, db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - db.mutate(|d| { - let mut ui = d.as_ui().de()?; - let parsed_url = Some(MAIN_REGISTRY.parse().unwrap()); - for package_id in d.as_package_data().keys()? { - if !COMMUNITY_SERVICES.contains(&&*package_id.to_string()) { - continue; - } - d.as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? - .as_marketplace_url_mut() - .ser(&parsed_url)?; - } - - if let imbl_value::Value::Object(ref mut obj) = ui { - obj.remove("theme"); - obj.remove("widgets"); - } - - ui["marketplace"]["known-hosts"][COMMUNITY_URL].take(); - ui["marketplace"]["known-hosts"][MAIN_REGISTRY].take(); - d.as_ui_mut().ser(&ui) - }) - .await - } -} diff --git a/core/startos/src/version/v0_3_4_1.rs b/core/startos/src/version/v0_3_4_1.rs deleted file mode 100644 index 915a47235..000000000 --- a/core/startos/src/version/v0_3_4_1.rs +++ /dev/null @@ -1,31 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; - -use super::v0_3_4::V0_3_0_COMPAT; -use super::*; -use crate::prelude::*; - -const V0_3_4_1: emver::Version = emver::Version::new(0, 3, 4, 1); - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = v0_3_4::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_3_4_1 - } - fn compat(&self) -> &'static VersionRange { - &*V0_3_0_COMPAT - } - async fn up(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } -} diff --git a/core/startos/src/version/v0_3_4_2.rs b/core/startos/src/version/v0_3_4_2.rs deleted file mode 100644 index 5931b2879..000000000 --- a/core/startos/src/version/v0_3_4_2.rs +++ /dev/null @@ -1,31 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; - -use super::v0_3_4::V0_3_0_COMPAT; -use super::*; -use crate::prelude::*; - -const V0_3_4_2: emver::Version = emver::Version::new(0, 3, 4, 2); - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = v0_3_4_1::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_3_4_2 - } - fn compat(&self) -> &'static VersionRange { - &*V0_3_0_COMPAT - } - async fn up(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } -} diff --git a/core/startos/src/version/v0_3_4_3.rs b/core/startos/src/version/v0_3_4_3.rs deleted file mode 100644 index d3199e913..000000000 --- a/core/startos/src/version/v0_3_4_3.rs +++ /dev/null @@ -1,31 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; - -use super::v0_3_4::V0_3_0_COMPAT; -use super::*; -use crate::prelude::*; - -const V0_3_4_3: emver::Version = emver::Version::new(0, 3, 4, 3); - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = v0_3_4_2::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_3_4_3 - } - fn compat(&self) -> &'static VersionRange { - &V0_3_0_COMPAT - } - async fn up(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } -} diff --git a/core/startos/src/version/v0_3_4_4.rs b/core/startos/src/version/v0_3_4_4.rs deleted file mode 100644 index b6345ca4c..000000000 --- a/core/startos/src/version/v0_3_4_4.rs +++ /dev/null @@ -1,43 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; -use models::ResultExt; -use sqlx::PgPool; - -use super::v0_3_4::V0_3_0_COMPAT; -use super::{v0_3_4_3, VersionT}; -use crate::prelude::*; - -const V0_3_4_4: emver::Version = emver::Version::new(0, 3, 4, 4); - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = v0_3_4_3::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_3_4_4 - } - fn compat(&self) -> &'static VersionRange { - &V0_3_0_COMPAT - } - async fn up(&self, db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - db.mutate(|v| { - let tor_address_lens = v.as_server_info_mut().as_tor_address_mut(); - let mut tor_addr = tor_address_lens.de()?; - tor_addr - .set_scheme("https") - .map_err(|_| eyre!("unable to update url scheme to https")) - .with_kind(crate::ErrorKind::ParseUrl)?; - tor_address_lens.ser(&tor_addr) - }) - .await?; - Ok(()) - } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } -} diff --git a/core/startos/src/version/v0_3_5.rs b/core/startos/src/version/v0_3_5.rs index ba28cd468..0217b6738 100644 --- a/core/startos/src/version/v0_3_5.rs +++ b/core/startos/src/version/v0_3_5.rs @@ -1,109 +1,49 @@ -use std::collections::BTreeMap; -use std::path::Path; +use exver::{ExtendedVersion, VersionRange}; -use async_trait::async_trait; -use emver::VersionRange; -use models::DataUrl; -use sqlx::PgPool; - -use super::v0_3_4::V0_3_0_COMPAT; -use super::{v0_3_4_4, VersionT}; +use super::VersionT; use crate::prelude::*; +use crate::version::Current; -const V0_3_5: emver::Version = emver::Version::new(0, 3, 5, 0); +lazy_static::lazy_static! { + pub static ref V0_3_0_COMPAT: VersionRange = VersionRange::and( + VersionRange::anchor( + exver::GTE, + ExtendedVersion::new( + exver::Version::new([0, 3, 0], []), + exver::Version::default(), + ), + ), + VersionRange::anchor( + exver::LTE, + ExtendedVersion::new( + Current::default().semver(), + exver::Version::default(), + ) + ), + ); + static ref V0_3_5: exver::Version = exver::Version::new([0, 3, 5], []); +} -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct Version; -#[async_trait] impl VersionT for Version { - type Previous = v0_3_4_4::Version; - fn new() -> Self { - Version + type Previous = Self; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) } - fn semver(&self) -> emver::Version { - V0_3_5 + fn semver(self) -> exver::Version { + V0_3_5.clone() } - fn compat(&self) -> &'static VersionRange { + fn compat(self) -> &'static VersionRange { &V0_3_0_COMPAT } - async fn up(&self, db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { - let peek = db.peek().await; - let mut url_replacements = BTreeMap::new(); - for (_, pde) in peek.as_package_data().as_entries()? { - for (dependency, info) in pde - .as_installed() - .map(|i| i.as_dependency_info().as_entries()) - .transpose()? - .into_iter() - .flatten() - { - if !url_replacements.contains_key(&dependency) { - url_replacements.insert( - dependency, - DataUrl::from_path( - <&Value>::from(info.as_icon()) - .as_str() - .and_then(|i| i.strip_prefix("/public/package-data/")) - .map(|path| { - Path::new("/embassy-data/package-data/public").join(path) - }) - .unwrap_or_default(), - ) - .await - .unwrap_or_else(|_| { - DataUrl::from_slice( - "image/png", - include_bytes!("../install/package-icon.png"), - ) - }), - ); - } - } - } - let prev_zram = db - .mutate(|v| { - for (_, pde) in v.as_package_data_mut().as_entries_mut()? { - for (dependency, info) in pde - .as_installed_mut() - .map(|i| i.as_dependency_info_mut().as_entries_mut()) - .transpose()? - .into_iter() - .flatten() - { - if let Some(url) = url_replacements.get(&dependency) { - info.as_icon_mut().ser(url)?; - } else { - info.as_icon_mut().ser(&DataUrl::from_slice( - "image/png", - include_bytes!("../install/package-icon.png"), - ))?; - } - let manifest = <&mut Value>::from(&mut *info) - .as_object_mut() - .and_then(|o| o.remove("manifest")); - if let Some(title) = manifest - .as_ref() - .and_then(|m| m.as_object()) - .and_then(|m| m.get("title")) - .and_then(|t| t.as_str()) - .map(|s| s.to_owned()) - { - info.as_title_mut().ser(&title)?; - } else { - info.as_title_mut().ser(&dependency.to_string())?; - } - } - } - v.as_server_info_mut().as_zram_mut().replace(&true) - }) - .await?; - if !prev_zram { - crate::system::enable_zram().await?; - } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { Ok(()) } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { + fn down(self, _db: &mut Value) -> Result<(), Error> { Ok(()) } } diff --git a/core/startos/src/version/v0_3_5_1.rs b/core/startos/src/version/v0_3_5_1.rs index c004dc8b6..5334cc2a4 100644 --- a/core/startos/src/version/v0_3_5_1.rs +++ b/core/startos/src/version/v0_3_5_1.rs @@ -1,32 +1,33 @@ -use async_trait::async_trait; -use emver::VersionRange; -use sqlx::PgPool; +use exver::VersionRange; -use super::v0_3_4::V0_3_0_COMPAT; +use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_5, VersionT}; use crate::prelude::*; -const V0_3_5_1: emver::Version = emver::Version::new(0, 3, 5, 1); +lazy_static::lazy_static! { + static ref V0_3_5_1: exver::Version = exver::Version::new([0, 3, 5, 1], []); +} -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct Version; -#[async_trait] impl VersionT for Version { type Previous = v0_3_5::Version; - fn new() -> Self { - Version + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) } - fn semver(&self) -> emver::Version { - V0_3_5_1 + fn semver(self) -> exver::Version { + V0_3_5_1.clone() } - fn compat(&self) -> &'static VersionRange { + fn compat(self) -> &'static VersionRange { &V0_3_0_COMPAT } - async fn up(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { Ok(()) } - async fn down(&self, _db: PatchDb, _secrets: &PgPool) -> Result<(), Error> { + fn down(self, _db: &mut Value) -> Result<(), Error> { Ok(()) } } diff --git a/core/startos/src/version/v0_3_5_2.rs b/core/startos/src/version/v0_3_5_2.rs new file mode 100644 index 000000000..780731d09 --- /dev/null +++ b/core/startos/src/version/v0_3_5_2.rs @@ -0,0 +1,33 @@ +use exver::VersionRange; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_5_1, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_5_2: exver::Version = exver::Version::new([0, 3, 5, 2], []); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_5_1::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_5_2.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_0.rs b/core/startos/src/version/v0_3_6_alpha_0.rs new file mode 100644 index 000000000..6ed3fc316 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_0.rs @@ -0,0 +1,512 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use chrono::{DateTime, Utc}; +use const_format::formatcp; +use ed25519_dalek::SigningKey; +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::{json, InternedString}; +use models::PackageId; +use openssl::pkey::PKey; +use openssl::x509::X509; +use sqlx::postgres::PgConnectOptions; +use sqlx::{PgPool, Row}; +use tokio::process::Command; +use torut::onion::TorSecretKeyV3; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_5_2, VersionT}; +use crate::account::AccountInfo; +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::context::RpcContext; +use crate::disk::mount::filesystem::cifs::Cifs; +use crate::disk::mount::util::unmount; +use crate::hostname::Hostname; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::{Notification, Notifications}; +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::ssh::{SshKeys, SshPubKey}; +use crate::util::crypto::ed25519_expand_key; +use crate::util::serde::{Pem, PemEncoding}; +use crate::util::Invoke; +use crate::{DATA_DIR, PACKAGE_DATA}; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_0: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 0.into()] + ); +} + +#[tracing::instrument(skip_all)] +async fn init_postgres(datadir: impl AsRef) -> Result { + let db_dir = datadir.as_ref().join("main/postgresql"); + if tokio::process::Command::new("mountpoint") + .arg("/var/lib/postgresql") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await? + .success() + { + unmount("/var/lib/postgresql", true).await?; + } + let exists = tokio::fs::metadata(&db_dir).await.is_ok(); + if !exists { + Command::new("cp") + .arg("-ra") + .arg("/var/lib/postgresql") + .arg(&db_dir) + .invoke(crate::ErrorKind::Filesystem) + .await?; + } + Command::new("chown") + .arg("-R") + .arg("postgres:postgres") + .arg(&db_dir) + .invoke(crate::ErrorKind::Database) + .await?; + + let mut pg_paths = tokio::fs::read_dir("/usr/lib/postgresql").await?; + let mut pg_version = None; + while let Some(pg_path) = pg_paths.next_entry().await? { + let pg_path_version = pg_path + .file_name() + .to_str() + .map(|v| v.parse()) + .transpose()? + .unwrap_or(0); + if pg_path_version > pg_version.unwrap_or(0) { + pg_version = Some(pg_path_version) + } + } + let pg_version = pg_version.ok_or_else(|| { + Error::new( + eyre!("could not determine postgresql version"), + crate::ErrorKind::Database, + ) + })?; + + crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?; + + let pg_version_string = pg_version.to_string(); + let pg_version_path = db_dir.join(&pg_version_string); + if exists + // maybe migrate + { + let incomplete_path = db_dir.join(format!("{pg_version}.migration.incomplete")); + if tokio::fs::metadata(&incomplete_path).await.is_ok() // previous migration was incomplete + && tokio::fs::metadata(&pg_version_path).await.is_ok() + { + tokio::fs::remove_dir_all(&pg_version_path).await?; + } + if tokio::fs::metadata(&pg_version_path).await.is_err() + // need to migrate + { + let conf_dir = Path::new("/etc/postgresql").join(pg_version.to_string()); + let conf_dir_tmp = { + let mut tmp = conf_dir.clone(); + tmp.set_extension("tmp"); + tmp + }; + if tokio::fs::metadata(&conf_dir).await.is_ok() { + Command::new("mv") + .arg(&conf_dir) + .arg(&conf_dir_tmp) + .invoke(ErrorKind::Filesystem) + .await?; + } + let mut old_version = pg_version; + while old_version > 13 + /* oldest pg version included in startos */ + { + old_version -= 1; + let old_datadir = db_dir.join(old_version.to_string()); + if tokio::fs::metadata(&old_datadir).await.is_ok() { + tokio::fs::File::create(&incomplete_path) + .await? + .sync_all() + .await?; + Command::new("pg_upgradecluster") + .arg(old_version.to_string()) + .arg("main") + .invoke(crate::ErrorKind::Database) + .await?; + break; + } + } + if tokio::fs::metadata(&conf_dir).await.is_ok() { + if tokio::fs::metadata(&conf_dir).await.is_ok() { + tokio::fs::remove_dir_all(&conf_dir).await?; + } + Command::new("mv") + .arg(&conf_dir_tmp) + .arg(&conf_dir) + .invoke(ErrorKind::Filesystem) + .await?; + } + tokio::fs::remove_file(&incomplete_path).await?; + } + if tokio::fs::metadata(&incomplete_path).await.is_ok() { + unreachable!() // paranoia + } + } + + Command::new("systemctl") + .arg("start") + .arg(format!("postgresql@{pg_version}-main.service")) + .invoke(crate::ErrorKind::Database) + .await?; + if !exists { + Command::new("sudo") + .arg("-u") + .arg("postgres") + .arg("createuser") + .arg("root") + .invoke(crate::ErrorKind::Database) + .await?; + Command::new("sudo") + .arg("-u") + .arg("postgres") + .arg("createdb") + .arg("secrets") + .arg("-O") + .arg("root") + .invoke(crate::ErrorKind::Database) + .await?; + } + + let secret_store = + PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root")).await?; + sqlx::migrate!() + .run(&secret_store) + .await + .with_kind(crate::ErrorKind::Database)?; + Ok(secret_store) +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_5_2::Version; + type PreUpRes = (AccountInfo, SshKeys, CifsTargets); + fn semver(self) -> exver::Version { + V0_3_6_alpha_0.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn pre_up(self) -> Result { + let pg = init_postgres(DATA_DIR).await?; + let account = previous_account_info(&pg).await?; + + let ssh_keys = previous_ssh_keys(&pg).await?; + + let cifs = previous_cifs(&pg).await?; + + Ok((account, ssh_keys, cifs)) + } + fn up(self, db: &mut Value, (account, ssh_keys, cifs): Self::PreUpRes) -> Result<(), Error> { + let wifi = json!({ + "infterface": db["server-info"]["wifi"]["interface"], + "ssids": db["server-info"]["wifi"]["ssids"], + "selected": db["server-info"]["wifi"]["selected"], + "last_region": db["server-info"]["wifi"]["last-region"], + }); + + let ip_info = { + let mut ip_info = json!({}); + let empty = Default::default(); + for (k, v) in db["server-info"]["ip-info"].as_object().unwrap_or(&empty) { + let k: &str = k.as_ref(); + ip_info[k] = json!({ + "ipv4Range": v["ipv4-range"], + "ipv6Range": v["ipv6-range"], + "ipv4": v["ipv4"], + "ipv6": v["ipv6"], + }); + } + ip_info + }; + + let status_info = json!({ + "backupProgress": db["server-info"]["status-info"]["backup-progress"], + "updated": db["server-info"]["status-info"]["updated"], + "updateProgress": db["server-info"]["status-info"]["update-progress"], + "shuttingDown": db["server-info"]["status-info"]["shutting-down"], + "restarting": db["server-info"]["status-info"]["restarting"], + }); + let server_info = { + let mut server_info = json!({ + "arch": db["server-info"]["arch"], + "platform": db["server-info"]["platform"], + "id": db["server-info"]["id"], + "hostname": db["server-info"]["hostname"], + "version": db["server-info"]["version"], + "versionCompat": db["server-info"]["eos-version-compat"], + "lastBackup": db["server-info"]["last-backup"], + "lanAddress": db["server-info"]["lan-address"], + }); + + server_info["postInitMigrationTodos"] = json!([]); + let tor_address: String = from_value(db["server-info"]["tor-address"].clone())?; + // Maybe we do this like the Public::init does + server_info["torAddress"] = json!(tor_address); + server_info["onionAddress"] = json!(tor_address + .replace("https://", "") + .replace("http://", "") + .replace(".onion/", "")); + server_info["ipInfo"] = ip_info; + server_info["statusInfo"] = status_info; + server_info["wifi"] = wifi; + server_info["unreadNotificationCount"] = + db["server-info"]["unread-notification-count"].clone(); + server_info["passwordHash"] = db["server-info"]["password-hash"].clone(); + + server_info["pubkey"] = db["server-info"]["pubkey"].clone(); + server_info["caFingerprint"] = db["server-info"]["ca-fingerprint"].clone(); + server_info["ntpSynced"] = db["server-info"]["ntp-synced"].clone(); + server_info["zram"] = db["server-info"]["zram"].clone(); + server_info["governor"] = db["server-info"]["governor"].clone(); + // This one should always be empty, doesn't exist in the previous. And the smtp is all single word key + server_info["smtp"] = db["server-info"]["smtp"].clone(); + server_info + }; + + let public = json!({ + "serverInfo": server_info, + "packageData": json!({}), + "ui": db["ui"], + }); + + let private = { + let mut value = json!({}); + value["keyStore"] = to_value(&KeyStore::new(&account)?)?; + value["password"] = to_value(&account.password)?; + value["compatS9pkKey"] = to_value(&crate::db::model::private::generate_compat_key())?; + value["sshPrivkey"] = to_value(Pem::new_ref(&account.ssh_key))?; + value["sshPubkeys"] = to_value(&ssh_keys)?; + value["availablePorts"] = to_value(&AvailablePorts::new())?; + value["sessions"] = to_value(&Sessions::new())?; + value["notifications"] = to_value(&Notifications::new())?; + value["cifs"] = to_value(&cifs)?; + value["packageStores"] = json!({}); + value + }; + let next: Value = json!({ + "public": public, + "private": private, + }); + + *db = next; + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Err(Error::new( + eyre!("downgrades prohibited"), + ErrorKind::InvalidRequest, + )) + } + + #[instrument(skip(self, ctx))] + /// MUST be idempotent, and is run after *all* db migrations + async fn post_up(self, ctx: &RpcContext) -> Result<(), Error> { + let path = Path::new(formatcp!("{PACKAGE_DATA}/archive/")); + if !path.is_dir() { + return Err(Error::new( + eyre!( + "expected path ({}) to be a directory", + path.to_string_lossy() + ), + ErrorKind::Filesystem, + )); + } + // Should be the name of the package + let mut paths = tokio::fs::read_dir(path).await?; + while let Some(path) = paths.next_entry().await? { + let path = path.path(); + if !path.is_dir() { + continue; + } + // Should be the version of the package + let mut paths = tokio::fs::read_dir(path).await?; + while let Some(path) = paths.next_entry().await? { + let path = path.path(); + if !path.is_dir() { + continue; + } + + // Should be s9pk + let mut paths = tokio::fs::read_dir(path).await?; + while let Some(path) = paths.next_entry().await? { + let path = path.path(); + if path.is_dir() { + continue; + } + + let package_s9pk = tokio::fs::File::open(path).await?; + let file = MultiCursorFile::open(&package_s9pk).await?; + + let key = ctx.db.peek().await.into_private().into_compat_s9pk_key(); + ctx.services + .install( + ctx.clone(), + || crate::s9pk::load(file.clone(), || Ok(key.de()?.0), None), + None::, + None, + ) + .await? + .await? + .await?; + } + } + } + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +async fn previous_cifs(pg: &sqlx::Pool) -> Result { + let cifs = sqlx::query(r#"SELECT * FROM cifs_shares"#) + .fetch_all(pg) + .await? + .into_iter() + .map(|row| { + let id: i32 = row.try_get("id")?; + Ok::<_, Error>(( + id, + Cifs { + hostname: row + .try_get("hostname") + .with_ctx(|_| (ErrorKind::Database, "hostname"))?, + path: row + .try_get::("path") + .with_ctx(|_| (ErrorKind::Database, "path"))? + .into(), + username: row + .try_get("username") + .with_ctx(|_| (ErrorKind::Database, "username"))?, + password: row + .try_get("password") + .with_ctx(|_| (ErrorKind::Database, "password"))?, + }, + )) + }) + .fold(Ok::<_, Error>(CifsTargets::default()), |cifs, data| { + let mut cifs = cifs?; + let (id, cif_value) = data?; + cifs.0.insert(id as u32, cif_value); + Ok(cifs) + })?; + Ok(cifs) +} + +#[tracing::instrument(skip_all)] +async fn previous_account_info(pg: &sqlx::Pool) -> Result { + let account_query = sqlx::query(r#"SELECT * FROM account"#) + .fetch_one(pg) + .await?; + let account = { + AccountInfo { + password: account_query + .try_get("password") + .with_ctx(|_| (ErrorKind::Database, "password"))?, + tor_keys: vec![TorSecretKeyV3::try_from( + if let Some(bytes) = account_query + .try_get::>, _>("tor_key") + .with_ctx(|_| (ErrorKind::Database, "tor_key"))? + { + <[u8; 64]>::try_from(bytes) + .map_err(|e| { + Error::new( + eyre!("expected vec of len 64, got len {}", e.len()), + ErrorKind::ParseDbField, + ) + }) + .with_ctx(|_| (ErrorKind::Database, "password.u8 64"))? + } else { + ed25519_expand_key( + &<[u8; 32]>::try_from(account_query.try_get::, _>("network_key")?) + .map_err(|e| { + Error::new( + eyre!("expected vec of len 32, got len {}", e.len()), + ErrorKind::ParseDbField, + ) + }) + .with_ctx(|_| (ErrorKind::Database, "password.u8 32"))?, + ) + }, + )?], + server_id: account_query + .try_get("server_id") + .with_ctx(|_| (ErrorKind::Database, "server_id"))?, + hostname: Hostname( + account_query + .try_get::("hostname") + .with_ctx(|_| (ErrorKind::Database, "hostname"))? + .into(), + ), + root_ca_key: PKey::private_key_from_pem( + &account_query + .try_get::("root_ca_key_pem") + .with_ctx(|_| (ErrorKind::Database, "root_ca_key_pem"))? + .as_bytes(), + ) + .with_ctx(|_| (ErrorKind::Database, "private_key_from_pem"))?, + root_ca_cert: X509::from_pem( + account_query + .try_get::("root_ca_cert_pem") + .with_ctx(|_| (ErrorKind::Database, "root_ca_cert_pem"))? + .as_bytes(), + ) + .with_ctx(|_| (ErrorKind::Database, "X509::from_pem"))?, + compat_s9pk_key: SigningKey::generate(&mut rand::thread_rng()), + ssh_key: ssh_key::PrivateKey::random( + &mut rand::thread_rng(), + ssh_key::Algorithm::Ed25519, + ) + .with_ctx(|_| (ErrorKind::Database, "X509::ssh_key::PrivateKey::random"))?, + } + }; + Ok(account) +} +#[tracing::instrument(skip_all)] +async fn previous_ssh_keys(pg: &sqlx::Pool) -> Result { + let ssh_query = sqlx::query(r#"SELECT * FROM ssh_keys"#) + .fetch_all(pg) + .await?; + let ssh_keys: SshKeys = { + let keys = ssh_query.into_iter().fold( + Ok::<_, Error>(BTreeMap::>::new()), + |ssh_keys, row| { + let mut ssh_keys = ssh_keys?; + let time = row + .try_get::("created_at") + .map_err(Error::from) + .and_then(|x| x.parse::>().with_kind(ErrorKind::Database)) + .with_ctx(|_| (ErrorKind::Database, "openssh_pubkey::created_at"))?; + let value: SshPubKey = row + .try_get::("openssh_pubkey") + .map_err(Error::from) + .and_then(|x| x.parse().map(SshPubKey).with_kind(ErrorKind::Database)) + .with_ctx(|_| (ErrorKind::Database, "openssh_pubkey"))?; + let data = WithTimeData { + created_at: time, + updated_at: time, + value, + }; + let fingerprint = row + .try_get::("fingerprint") + .with_ctx(|_| (ErrorKind::Database, "fingerprint"))?; + ssh_keys.insert(fingerprint.into(), data); + Ok(ssh_keys) + }, + )?; + SshKeys::from(keys) + }; + Ok(ssh_keys) +} diff --git a/core/startos/src/version/v0_3_6_alpha_1.rs b/core/startos/src/version/v0_3_6_alpha_1.rs new file mode 100644 index 000000000..682486439 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_1.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_0, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_1: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 1.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_0::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_1.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_10.rs b/core/startos/src/version/v0_3_6_alpha_10.rs new file mode 100644 index 000000000..d81fc91ca --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_10.rs @@ -0,0 +1,94 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::InternedString; +use serde::{Deserialize, Serialize}; +use torut::onion::OnionAddressV3; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_9, VersionT}; +use crate::net::host::address::DomainConfig; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_10: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 10.into()] + ); +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "kind")] +enum HostAddress { + Onion { address: OnionAddressV3 }, + Domain { address: InternedString }, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_9::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_10.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + for (_, package) in db["public"]["packageData"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + for (_, host) in package["hosts"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData[id].hosts to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + let mut onions = BTreeSet::new(); + let mut domains = BTreeMap::new(); + let addresses = from_value::>(host["addresses"].clone())?; + for address in addresses { + match address { + HostAddress::Onion { address } => { + onions.insert(address); + } + HostAddress::Domain { address } => { + domains.insert( + address, + DomainConfig { + public: true, + acme: None, + }, + ); + } + } + } + host["onions"] = to_value(&onions)?; + host["domains"] = to_value(&domains)?; + } + } + + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_11.rs b/core/startos/src/version/v0_3_6_alpha_11.rs new file mode 100644 index 000000000..8a6dedd6e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_11.rs @@ -0,0 +1,83 @@ +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_10, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_11: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 11.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_10::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_11.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + let acme = std::mem::replace( + &mut db["public"]["serverInfo"]["acme"], + Value::Object(Default::default()), + ); + if !acme.is_null() && acme["provider"].as_str().is_some() { + db["public"]["serverInfo"]["acme"] + [&acme["provider"].as_str().or_not_found("provider")?] = + json!({ "contact": &acme["contact"] }); + } + + for (_, package) in db["public"]["packageData"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + for (_, host) in package["hosts"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData[id].hosts to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + for (_, bind) in host["bindings"] + .as_object_mut() + .ok_or_else(|| { + Error::new( + eyre!("expected public.packageData[id].hosts[hostId].bindings to be an object"), + ErrorKind::Database, + ) + })? + .iter_mut() + { + bind["net"] = bind["lan"].clone(); + bind["net"]["public"] = Value::Bool(false); + } + } + } + + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_12.rs b/core/startos/src/version/v0_3_6_alpha_12.rs new file mode 100644 index 000000000..3508a27ac --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_12.rs @@ -0,0 +1,68 @@ +use std::collections::BTreeMap; + +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_11, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_12: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 12.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_11::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_12.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + let bindings: BTreeMap = [( + 80, + json!({ + "enabled": false, + "options": { + "preferredExternalPort": 80, + "addSsl": { + "preferredExternalPort": 443, + "alpn": { "specified": [ "http/1.1", "h2" ] }, + }, + "secure": null, + }, + "net": { + "assignedPort": null, + "assignedSslPort": 443, + "public": false, + } + }), + )] + .into_iter() + .collect(); + let onion = db["public"]["serverInfo"]["onionAddress"].clone(); + db["public"]["serverInfo"]["host"] = json!({ + "bindings": bindings, + "onions": [onion], + "domains": {}, + "hostnameInfo": {}, + }); + + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_13.rs b/core/startos/src/version/v0_3_6_alpha_13.rs new file mode 100644 index 000000000..da34f7743 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_13.rs @@ -0,0 +1,39 @@ +use std::collections::BTreeMap; + +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_12, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_13: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 13.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_12::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_13.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_2.rs b/core/startos/src/version/v0_3_6_alpha_2.rs new file mode 100644 index 000000000..cddcc44b2 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_2.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_1, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_2: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 2.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_1::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_2.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_3.rs b/core/startos/src/version/v0_3_6_alpha_3.rs new file mode 100644 index 000000000..90164ad60 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_3.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_2, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_3: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 3.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_2::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_3.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_4.rs b/core/startos/src/version/v0_3_6_alpha_4.rs new file mode 100644 index 000000000..08ff7595e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_4.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_3, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_4: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 4.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_3::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_4.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_5.rs b/core/startos/src/version/v0_3_6_alpha_5.rs new file mode 100644 index 000000000..649fe90ca --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_5.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_4, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_5: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 5.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_4::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_5.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_6.rs b/core/startos/src/version/v0_3_6_alpha_6.rs new file mode 100644 index 000000000..7d62773ea --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_6.rs @@ -0,0 +1,54 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_5, VersionT}; +use crate::notifications::{notify, NotificationLevel}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_6: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 6.into()] + ); +} + +#[derive(Default, Clone, Copy, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_5::Version; + type PreUpRes = (); + fn semver(self) -> exver::Version { + V0_3_6_alpha_6.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn pre_up(self) -> Result { + Ok(()) + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + async fn post_up<'a>(self, ctx: &'a crate::context::RpcContext) -> Result<(), Error> { + let message_update = include_str!("update_details/v0_3_6.md").to_string(); + + ctx.db + .mutate(|db| { + notify( + db, + None, + NotificationLevel::Success, + "Welcome to StartOS 0.3.6!".to_string(), + "Click \"View Details\" to learn all about the new version".to_string(), + message_update, + )?; + Ok(()) + }) + .await?; + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_7.rs b/core/startos/src/version/v0_3_6_alpha_7.rs new file mode 100644 index 000000000..baf7aaa35 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_7.rs @@ -0,0 +1,63 @@ +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; +use tokio::process::Command; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_6, VersionT}; +use crate::prelude::*; +use crate::util::Invoke; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_7: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 7.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_6::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_7.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + let server_info = db["public"]["serverInfo"] + .as_object_mut() + .or_not_found("public.serverInfo")?; + server_info.insert("ram".into(), json!(0)); + server_info.insert("devices".into(), json!([])); + let package_data = db["public"]["packageData"] + .as_object_mut() + .or_not_found("public.packageData")?; + for (_, pde) in package_data.iter_mut() { + if let Some(manifest) = pde["stateInfo"].get_mut("manifest") { + manifest["hardwareRequirements"]["device"] = json!([]); + } + } + Ok(()) + } + async fn post_up(self, ctx: &crate::context::RpcContext) -> Result<(), Error> { + Command::new("systemd-firstboot") + .arg("--root=/media/startos/config/overlay/") + .arg(format!( + "--hostname={}", + ctx.account.read().await.hostname.0 + )) + .invoke(ErrorKind::ParseSysInfo) + .await?; + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_8.rs b/core/startos/src/version/v0_3_6_alpha_8.rs new file mode 100644 index 000000000..99920c767 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_8.rs @@ -0,0 +1,133 @@ +use std::path::Path; + +use exver::{PreReleaseSegment, VersionRange}; +use tokio::fs::File; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_7, VersionT}; +use crate::install::PKG_ARCHIVE_DIR; +use crate::prelude::*; +use crate::s9pk::manifest::{DeviceFilter, Manifest}; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::MerkleArchive; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::s9pk::S9pk; +use crate::service::LoadDisposition; +use crate::util::io::create_file; +use crate::DATA_DIR; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_8: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 8.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_7::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_8.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + async fn post_up(self, ctx: &crate::context::RpcContext) -> Result<(), Error> { + let s9pk_dir = Path::new(DATA_DIR).join(PKG_ARCHIVE_DIR).join("installed"); + + if tokio::fs::metadata(&s9pk_dir).await.is_ok() { + let mut read_dir = tokio::fs::read_dir(&s9pk_dir).await?; + while let Some(s9pk_ent) = read_dir.next_entry().await? { + let s9pk_path = s9pk_ent.path(); + let matches_s9pk = s9pk_path.extension().map(|x| x == "s9pk").unwrap_or(false); + if !matches_s9pk { + continue; + } + + let get_archive = async { + let multi_cursor = MultiCursorFile::from(File::open(&s9pk_path).await?); + Ok::<_, Error>(S9pk::archive(&multi_cursor, None).await?) + }; + + let archive: MerkleArchive< + crate::s9pk::merkle_archive::source::Section, + > = match get_archive.await { + Ok(a) => a, + Err(e) => { + tracing::error!("Error opening s9pk for install: {e}"); + tracing::debug!("{e:?}"); + continue; + } + }; + + let previous_manifest: Value = serde_json::from_slice::( + &archive + .contents() + .get_path("manifest.json") + .or_not_found("manifest.json")? + .read_file_to_vec() + .await?, + ) + .with_kind(ErrorKind::Deserialization)? + .into(); + + let mut manifest = previous_manifest.clone(); + + if let Some(device) = + previous_manifest["hardwareRequirements"]["device"].as_object() + { + manifest["hardwareRequirements"]["device"] = to_value( + &device + .into_iter() + .map(|(class, product)| { + Ok::<_, Error>(DeviceFilter { + pattern_description: format!( + "a {class} device matching the expression {}", + &product + ), + class: class.clone(), + pattern: from_value(product.clone())?, + }) + }) + .fold(Ok::<_, Error>(Vec::new()), |acc, value| { + let mut acc = acc?; + acc.push(value?); + Ok(acc) + })?, + )?; + } + + if previous_manifest != manifest { + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + let mut tmp_file = create_file(&tmp_path).await?; + // TODO, wouldn't this break in the later versions of the manifest that would need changes, this doesn't seem to be a good way to handle this + let manifest: Manifest = from_value(manifest.clone())?; + let id = manifest.id.clone(); + let mut s9pk: S9pk<_> = S9pk::new_with_manifest(archive, None, manifest); + let s9pk_compat_key = ctx.account.read().await.compat_s9pk_key.clone(); + s9pk.as_archive_mut() + .set_signer(s9pk_compat_key, SIG_CONTEXT); + s9pk.serialize(&mut tmp_file, true).await?; + tmp_file.sync_all().await?; + tokio::fs::rename(&tmp_path, &s9pk_path).await?; + ctx.services.load(ctx, &id, LoadDisposition::Retry).await?; + } + } + } + + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_9.rs b/core/startos/src/version/v0_3_6_alpha_9.rs new file mode 100644 index 000000000..e01ad298e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_9.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_8, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_9: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 9.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_8::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_9.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/volume.rs b/core/startos/src/volume.rs index 1633b7d18..113802286 100644 --- a/core/startos/src/volume.rs +++ b/core/startos/src/volume.rs @@ -1,94 +1,14 @@ -use std::collections::BTreeMap; -use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; pub use helpers::script_dir; +use models::PackageId; pub use models::VolumeId; -use serde::{Deserialize, Serialize}; -use tracing::instrument; -use crate::context::RpcContext; -use crate::net::interface::{InterfaceId, Interfaces}; -use crate::net::PACKAGE_CERT_PATH; use crate::prelude::*; -use crate::s9pk::manifest::PackageId; -use crate::util::Version; -use crate::{Error, ResultExt}; +use crate::util::VersionString; pub const PKG_VOLUME_DIR: &str = "package-data/volumes"; -pub const BACKUP_DIR: &str = "/media/embassy/backups"; - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct Volumes(BTreeMap); -impl Volumes { - #[instrument(skip_all)] - pub fn validate(&self, interfaces: &Interfaces) -> Result<(), Error> { - for (id, volume) in &self.0 { - volume - .validate(interfaces) - .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, format!("Volume {}", id)))?; - if let Volume::Backup { .. } = volume { - return Err(Error::new( - eyre!("Invalid volume type \"backup\""), - ErrorKind::ParseS9pk, - )); // Volume::Backup is for internal use and shouldn't be declared in manifest - } - } - Ok(()) - } - #[instrument(skip_all)] - pub async fn install( - &self, - ctx: &RpcContext, - pkg_id: &PackageId, - version: &Version, - ) -> Result<(), Error> { - for (volume_id, volume) in &self.0 { - volume - .install(&ctx.datadir, pkg_id, version, volume_id) - .await?; // TODO: concurrent? - } - Ok(()) - } - pub fn get_path_for( - &self, - path: &PathBuf, - pkg_id: &PackageId, - version: &Version, - volume_id: &VolumeId, - ) -> Option { - self.0 - .get(volume_id) - .map(|volume| volume.path_for(path, pkg_id, version, volume_id)) - } - pub fn to_readonly(&self) -> Self { - Volumes( - self.0 - .iter() - .map(|(id, volume)| { - let mut volume = volume.clone(); - volume.set_readonly(); - (id.clone(), volume) - }) - .collect(), - ) - } -} -impl Deref for Volumes { - type Target = BTreeMap; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl DerefMut for Volumes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} -impl Map for Volumes { - type Key = VolumeId; - type Value = Volume; -} +pub const BACKUP_DIR: &str = "/media/startos/backups"; pub fn data_dir>(datadir: P, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf { datadir @@ -99,7 +19,11 @@ pub fn data_dir>(datadir: P, pkg_id: &PackageId, volume_id: &Volu .join(volume_id) } -pub fn asset_dir>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf { +pub fn asset_dir>( + datadir: P, + pkg_id: &PackageId, + version: &VersionString, +) -> PathBuf { datadir .as_ref() .join(PKG_VOLUME_DIR) @@ -111,130 +35,3 @@ pub fn asset_dir>(datadir: P, pkg_id: &PackageId, version: &Versi pub fn backup_dir(pkg_id: &PackageId) -> PathBuf { Path::new(BACKUP_DIR).join(pkg_id).join("data") } - -pub fn cert_dir(pkg_id: &PackageId, interface_id: &InterfaceId) -> PathBuf { - Path::new(PACKAGE_CERT_PATH).join(pkg_id).join(interface_id) -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type")] -#[serde(rename_all = "kebab-case")] -pub enum Volume { - #[serde(rename_all = "kebab-case")] - Data { - #[serde(skip)] - readonly: bool, - }, - #[serde(rename_all = "kebab-case")] - Assets {}, - #[serde(rename_all = "kebab-case")] - Pointer { - package_id: PackageId, - volume_id: VolumeId, - path: PathBuf, - readonly: bool, - }, - #[serde(rename_all = "kebab-case")] - Certificate { interface_id: InterfaceId }, - #[serde(rename_all = "kebab-case")] - Backup { readonly: bool }, -} -impl Volume { - #[instrument(skip_all)] - pub fn validate(&self, interfaces: &Interfaces) -> Result<(), color_eyre::eyre::Report> { - match self { - Volume::Certificate { interface_id } => { - if !interfaces.0.contains_key(interface_id) { - color_eyre::eyre::bail!("unknown interface: {}", interface_id); - } - } - _ => (), - } - Ok(()) - } - pub async fn install( - &self, - path: &PathBuf, - pkg_id: &PackageId, - version: &Version, - volume_id: &VolumeId, - ) -> Result<(), Error> { - match self { - Volume::Data { .. } => { - tokio::fs::create_dir_all(self.path_for(path, pkg_id, version, volume_id)).await?; - } - _ => (), - } - Ok(()) - } - pub fn path_for( - &self, - data_dir_path: impl AsRef, - pkg_id: &PackageId, - version: &Version, - volume_id: &VolumeId, - ) -> PathBuf { - match self { - Volume::Data { .. } => data_dir(&data_dir_path, pkg_id, volume_id), - Volume::Assets {} => asset_dir(&data_dir_path, pkg_id, version).join(volume_id), - Volume::Pointer { - package_id, - volume_id, - path, - .. - } => data_dir(&data_dir_path, package_id, volume_id).join(if path.is_absolute() { - path.strip_prefix("/").unwrap() - } else { - path.as_ref() - }), - Volume::Certificate { interface_id } => cert_dir(pkg_id, &interface_id), - Volume::Backup { .. } => backup_dir(pkg_id), - } - } - - pub fn pointer_path(&self, data_dir_path: impl AsRef) -> Option { - if let Volume::Pointer { - path, - package_id, - volume_id, - .. - } = self - { - Some( - data_dir(data_dir_path.as_ref(), package_id, volume_id).join( - if path.is_absolute() { - path.strip_prefix("/").unwrap() - } else { - path.as_ref() - }, - ), - ) - } else { - None - } - } - - pub fn set_readonly(&mut self) { - match self { - Volume::Data { readonly } => { - *readonly = true; - } - Volume::Pointer { readonly, .. } => { - *readonly = true; - } - Volume::Backup { readonly } => { - *readonly = true; - } - _ => (), - } - } - pub fn readonly(&self) -> bool { - match self { - Volume::Data { readonly } => *readonly, - Volume::Assets {} => true, - Volume::Pointer { readonly, .. } => *readonly, - Volume::Certificate { .. } => true, - Volume::Backup { readonly } => *readonly, - } - } -} diff --git a/core/startos/startd.service b/core/startos/startd.service index 894298e54..6ce17697e 100644 --- a/core/startos/startd.service +++ b/core/startos/startd.service @@ -1,12 +1,9 @@ [Unit] Description=StartOS Daemon -After=network-online.target -Requires=network-online.target -Wants=avahi-daemon.service [Service] Type=simple -Environment=RUST_LOG=startos=debug,js_engine=debug,patch_db=warn +Environment=RUST_LOG=startos=debug,patch_db=warn ExecStart=/usr/bin/startd Restart=always RestartSec=3 diff --git a/debian/postinst b/debian/postinst index 6a65a749d..d29fcfb86 100755 --- a/debian/postinst +++ b/debian/postinst @@ -6,12 +6,12 @@ if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then SYSTEMCTL=deb-systemd-helper fi -if [ -f /usr/sbin/grub-probe ]; then +if [ -f /usr/sbin/grub-probe ] && ! [ -L /usr/sbin/grub-probe ]; then mv /usr/sbin/grub-probe /usr/sbin/grub-probe-default ln -s /usr/lib/startos/scripts/grub-probe-eos /usr/sbin/grub-probe fi -cp /usr/lib/startos/scripts/embassy-initramfs-module /etc/initramfs-tools/scripts/embassy +cp /usr/lib/startos/scripts/startos-initramfs-module /etc/initramfs-tools/scripts/startos if ! grep overlay /etc/initramfs-tools/modules > /dev/null; then echo overlay >> /etc/initramfs-tools/modules @@ -20,10 +20,19 @@ fi update-initramfs -u -k all if [ -f /etc/default/grub ]; then - sed -i '/\(^\|#\)GRUB_CMDLINE_LINUX=/c\GRUB_CMDLINE_LINUX="boot=embassy"' /etc/default/grub + sed -i '/\(^\|#\)GRUB_CMDLINE_LINUX=/c\GRUB_CMDLINE_LINUX="boot=startos console=ttyS0,115200n8"' /etc/default/grub sed -i '/\(^\|#\)GRUB_DISTRIBUTOR=/c\GRUB_DISTRIBUTOR="StartOS v$(cat /usr/lib/startos/VERSION.txt)"' /etc/default/grub + sed -i '/\(^\|#\)GRUB_TERMINAL=/c\GRUB_TERMINAL="serial"\nGRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' /etc/default/grub fi +# set local and remote login prompt +cat << EOF > /etc/issue +StartOS v$(cat /usr/lib/startos/VERSION.txt) [\\m] on \\n.local (\\l) +EOF +cat << EOF > /etc/issue.net +StartOS v$(cat /usr/lib/startos/VERSION.txt) +EOF + # change timezone rm -f /etc/localtime ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime @@ -49,9 +58,9 @@ managed=true EOF $SYSTEMCTL enable startd.service $SYSTEMCTL enable systemd-resolved.service -$SYSTEMCTL enable systemd-networkd-wait-online.service $SYSTEMCTL enable ssh.service $SYSTEMCTL disable wpa_supplicant.service +$SYSTEMCTL mask systemd-networkd-wait-online.service # currently use `NetworkManager-wait-online.service` $SYSTEMCTL disable docker.service $SYSTEMCTL disable postgresql.service @@ -78,46 +87,58 @@ sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf sed -i '/^\s*#\?\s*issue_discards\s*=\s*/c\issue_discards = 1' /etc/lvm/lvm.conf +sed -i '/\(^\|#\)\s*unqualified-search-registries\s*=\s*/c\unqualified-search-registries = ["docker.io"]' /etc/containers/registries.conf +sed -i 's/\(#\|\^\)\s*\([^=]\+\)=\(suspend\|hibernate\)\s*$/\2=ignore/g' /etc/systemd/logind.conf +sed -i '/\(^\|#\)MulticastDNS=/c\MulticastDNS=no' /etc/systemd/resolved.conf +sed -i 's/\[Service\]/[Service]\nEnvironment=SYSTEMD_LOG_LEVEL=debug/' /lib/systemd/system/systemd-timesyncd.service +sed -i '/\(^\|#\)RootDistanceMaxSec=/c\RootDistanceMaxSec=10' /etc/systemd/timesyncd.conf -if cat /usr/lib/startos/ENVIRONMENT.txt | grep '(^|-)docker(-|$)'; then - sed -i 's/ExecStart=\/usr\/bin\/dockerd/ExecStart=\/usr\/bin\/dockerd --exec-opt native.cgroupdriver=systemd/g' /lib/systemd/system/docker.service - mkdir -p /etc/docker - echo '{ "storage-driver": "overlay2" }' > /etc/docker/daemon.json -else - podman network create -d bridge --subnet 172.18.0.1/24 --opt com.docker.network.bridge.name=br-start9 start9 -fi mkdir -p /etc/nginx/ssl -# fix to suppress docker warning, fixed in 21.xx release of docker cli: https://github.com/docker/cli/pull/2934 -mkdir -p /root/.docker -touch /root/.docker/config.json - cat << EOF > /etc/tor/torrc SocksPort 0.0.0.0:9050 SocksPolicy accept 127.0.0.1 SocksPolicy accept 172.18.0.0/16 +SocksPolicy accept 10.0.3.0/24 SocksPolicy reject * ControlPort 9051 CookieAuthentication 1 EOF rm -rf /var/lib/tor/* -ln -sf /usr/lib/startos/scripts/tor-check.sh /usr/bin/tor-check +ln -sf /usr/lib/startos/scripts/chroot-and-upgrade /usr/bin/chroot-and-upgrade +ln -sf /usr/lib/startos/scripts/tor-check /usr/bin/tor-check +ln -sf /usr/lib/startos/scripts/gather-debug-info /usr/bin/gather-debug-info +ln -sf /usr/lib/startos/scripts/wg-vps-setup /usr/bin/wg-vps-setup -echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-embassy.conf +echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-startos.conf # Old pi was set with this locale, because of pg we are now stuck with including that locale locale-gen en_GB en_GB.UTF-8 echo "locales locales/locales_to_be_generated multiselect en_GB.UTF-8 UTF-8" | debconf-set-selections update-locale LANGUAGE -rm "/etc/locale.gen" +rm -f "/etc/locale.gen" dpkg-reconfigure --frontend noninteractive locales -groupadd embassy - -ln -s /usr/lib/startos/scripts/dhclient-exit-hook /etc/dhcp/dhclient-exit-hooks.d/embassy +if ! getent group | grep '^startos:'; then + groupadd startos +fi rm -f /etc/motd -ln -sf /usr/lib/startos/motd /etc/update-motd.d/00-embassy +ln -sf /usr/lib/startos/motd /etc/update-motd.d/00-startos chmod -x /etc/update-motd.d/* -chmod +x /etc/update-motd.d/00-embassy +chmod +x /etc/update-motd.d/00-startos + +# LXC +cat /etc/subuid | grep -v '^root:' > /etc/subuid.tmp || true +echo "root:100000:65536" >> /etc/subuid.tmp +mv /etc/subuid.tmp /etc/subuid + +cat /etc/subgid | grep -v '^root:' > /etc/subgid.tmp || true +echo "root:100000:65536" >> /etc/subgid.tmp +mv /etc/subgid.tmp /etc/subgid + +cat /etc/lxc/default.conf | grep -v '^lxc\.idmap = [ug]' > /etc/lxc/default.conf.tmp || true +echo "lxc.idmap = u 0 100000 65536" >> /etc/lxc/default.conf.tmp +echo "lxc.idmap = g 0 100000 65536" >> /etc/lxc/default.conf.tmp +mv /etc/lxc/default.conf.tmp /etc/lxc/default.conf \ No newline at end of file diff --git a/devmode.sh b/devmode.sh new file mode 100755 index 000000000..19b0651de --- /dev/null +++ b/devmode.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +export ENVIRONMENT=dev +export GIT_BRANCH_AS_HASH=1 diff --git a/download-firmware.sh b/download-firmware.sh index 2457b3062..be72e6a6d 100755 --- a/download-firmware.sh +++ b/download-firmware.sh @@ -16,7 +16,8 @@ mkdir -p ./firmware/$PLATFORM cd ./firmware/$PLATFORM -mapfile -t firmwares <<< "$(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)" +firmwares=() +while IFS= read -r line; do firmwares+=("$line"); done < <(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json) for firmware in "${firmwares[@]}"; do if [ -n "$firmware" ]; then id=$(echo "$firmware" | jq --raw-output '.id') diff --git a/dpkg-build.sh b/dpkg-build.sh index e8ffdb0ac..783c205b3 100755 --- a/dpkg-build.sh +++ b/dpkg-build.sh @@ -17,6 +17,7 @@ fi rm -rf dpkg-workdir/$BASENAME mkdir -p dpkg-workdir/$BASENAME +make make install DESTDIR=dpkg-workdir/$BASENAME DEPENDS=$(cat dpkg-workdir/$BASENAME/usr/lib/startos/depends | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//') diff --git a/image-recipe/README.md b/image-recipe/README.md index cbaf8944a..9eba04727 100644 --- a/image-recipe/README.md +++ b/image-recipe/README.md @@ -8,13 +8,9 @@ official StartOS images, you can use the `run-local-build.sh` helper script: ```bash # Prerequisites -sudo apt-get install -y debspawn +sudo apt-get install -y debspawn binfmt-support sudo mkdir -p /etc/debspawn/ && echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml -# Get dpkg -mkdir -p overlays/startos/root -wget -O overlays/startos/root/startos_0.3.x-1_amd64.deb - # Build image ./run-local-build.sh ``` diff --git a/image-recipe/build.sh b/image-recipe/build.sh index 3ff91ca75..eaf5e8382 100755 --- a/image-recipe/build.sh +++ b/image-recipe/build.sh @@ -18,10 +18,6 @@ echo "Saving results in: $RESULTS_DIR" IMAGE_BASENAME=startos-${VERSION_FULL}_${IB_TARGET_PLATFORM} -mkdir -p $prep_results_dir - -cd $prep_results_dir - QEMU_ARCH=${IB_TARGET_ARCH} BOOTLOADERS=grub-efi,syslinux if [ "$QEMU_ARCH" = 'amd64' ]; then @@ -30,6 +26,19 @@ elif [ "$QEMU_ARCH" = 'arm64' ]; then QEMU_ARCH=aarch64 BOOTLOADERS=grub-efi fi + +# TODO: remove when util-linux is released at v2.39 +cd $base_dir +git clone --depth=1 --branch=v2.39.3 https://github.com/util-linux/util-linux.git +cd util-linux +./autogen.sh +CC=$QEMU_ARCH-linux-gnu-gcc ./configure --host=$QEMU_ARCH-linux-gnu --disable-all-programs --enable-mount --enable-libmount --enable-libblkid --enable-libuuid --enable-static-programs +CC=$QEMU_ARCH-linux-gnu-gcc make -j mount.static + +mkdir -p $prep_results_dir + +cd $prep_results_dir + NON_FREE= if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then NON_FREE=1 @@ -48,22 +57,17 @@ if [ "$NON_FREE" = 1 ]; then fi fi -PLATFORM_CONFIG_EXTRAS= +PLATFORM_CONFIG_EXTRAS=() if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then - PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-binary false" - PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-chroot false" - # BEGIN stupid ugly hack - # The actual name of the package is `raspberrypi-kernel` - # live-build determines thte name of the package for the kernel by combining the `linux-packages` flag, with the `linux-flavours` flag - # the `linux-flavours` flag defaults to the architecture, so there's no way to remove the suffix. - # So we're doing this, cause thank the gods our package name contains a hypen. Cause if it didn't we'd be SOL - PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-packages raspberrypi" - PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours kernel" - # END stupid ugly hack + PLATFORM_CONFIG_EXTRAS+=( --firmware-binary false ) + PLATFORM_CONFIG_EXTRAS+=( --firmware-chroot false ) + PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-6.6.51+rpt ) + PLATFORM_CONFIG_EXTRAS+=( --linux-flavours "rpi-v8 rpi-2712" ) elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then - PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours rockchip64" + PLATFORM_CONFIG_EXTRAS+=( --linux-flavours rockchip64 ) fi + cat > /etc/wgetrc << EOF retry_connrefused = on tries = 100 @@ -84,13 +88,16 @@ lb config \ --bootstrap-qemu-arch ${IB_TARGET_ARCH} \ --bootstrap-qemu-static /usr/bin/qemu-${QEMU_ARCH}-static \ --archive-areas "${ARCHIVE_AREAS}" \ - $PLATFORM_CONFIG_EXTRAS + ${PLATFORM_CONFIG_EXTRAS[@]} # Overlays mkdir -p config/includes.chroot/deb cp $base_dir/deb/${IMAGE_BASENAME}.deb config/includes.chroot/deb/ +mkdir -p config/includes.chroot/usr/local/bin +cp $base_dir/util-linux/mount.static config/includes.chroot/usr/local/bin/mount.next + if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then cp -r $base_dir/raspberrypi/squashfs/* config/includes.chroot/ fi @@ -135,17 +142,15 @@ sed -i -e '2i set timeout=5' config/bootloaders/grub-pc/config.cfg mkdir -p config/archives if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then - curl -fsSL https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o config/archives/raspi.key - echo "deb https://archive.raspberrypi.org/debian/ bullseye main" > config/archives/raspi.list + curl -fsSL https://archive.raspberrypi.com/debian/raspberrypi.gpg.key | gpg --dearmor -o config/archives/raspi.key + echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/raspi.key.gpg] https://archive.raspberrypi.com/debian/ ${IB_SUITE} main" > config/archives/raspi.list fi -if [ "${IB_SUITE}" = "bullseye" ]; then - cat > config/archives/backports.pref <<- EOF - Package: * - Pin: release a=bullseye-backports - Pin-Priority: 500 - EOF -fi +cat > config/archives/backports.pref <<- EOF +Package: * +Pin: release n=${IB_SUITE}-backports +Pin-Priority: 500 +EOF if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then curl -fsSL https://apt.armbian.com/armbian.key | gpg --dearmor -o config/archives/armbian.key @@ -158,21 +163,10 @@ echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg] curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o config/archives/docker.key echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/docker.key.gpg] https://download.docker.com/linux/debian ${IB_SUITE} stable" > config/archives/docker.list -echo "deb http://deb.debian.org/debian/ trixie main contrib" > config/archives/trixie.list -cat > config/archives/trixie.pref <<- EOF -Package: * -Pin: release n=trixie -Pin-Priority: 100 - -Package: podman -Pin: release n=trixie -Pin-Priority: 600 -EOF - # Dependencies ## Base dependencies -dpkg-deb --fsys-tarfile $base_dir/deb/${IMAGE_BASENAME}.deb | tar --to-stdout -xvf - ./usr/lib/startos/depends > config/package-lists/embassy-depends.list.chroot +dpkg-deb --fsys-tarfile $base_dir/deb/${IMAGE_BASENAME}.deb | tar --to-stdout -xvf - ./usr/lib/startos/depends > config/package-lists/startos-depends.list.chroot ## Firmware if [ "$NON_FREE" = 1 ]; then @@ -180,7 +174,7 @@ if [ "$NON_FREE" = 1 ]; then fi if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then - echo 'raspberrypi-bootloader rpi-update parted' > config/package-lists/bootloader.list.chroot + echo 'raspberrypi-net-mods raspberrypi-sys-mods raspi-config raspi-firmware raspi-gpio raspi-utils rpi-eeprom rpi-update rpi.gpio-common parted' > config/package-lists/bootloader.list.chroot else echo 'grub-efi grub2-common' > config/package-lists/bootloader.list.chroot fi @@ -205,16 +199,18 @@ if [ "${IB_SUITE}" = bookworm ]; then fi if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then + ln -sf /usr/bin/pi-beep /usr/local/bin/beep + SKIP_WARNING=1 SKIP_BOOTLOADER=1 SKIP_CHECK_PARTITION=1 WANT_64BIT=1 WANT_PI4=1 WANT_PI5=1 BOOT_PART=/boot rpi-update stable for f in /usr/lib/modules/*; do v=\${f#/usr/lib/modules/} echo "Configuring raspi kernel '\$v'" extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v - update-initramfs -c -k \$v done - ln -sf /usr/bin/pi-beep /usr/local/bin/beep + mkinitramfs -c gzip -o /boot/initramfs8 6.6.74-v8+ + mkinitramfs -c gzip -o /boot/initramfs_2712 6.6.74-v8-16k+ fi -useradd --shell /bin/bash -G embassy -m start9 +useradd --shell /bin/bash -G startos -m start9 echo start9:embassy | chpasswd usermod -aG sudo start9 @@ -313,18 +309,31 @@ elif [ "${IMAGE_TYPE}" = img ]; then TMPDIR=$(mktemp -d) - mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR - mkdir $TMPDIR/boot + mkdir -p $TMPDIR/boot $TMPDIR/root + mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR/root mount `partition_for ${OUTPUT_DEVICE} 1` $TMPDIR/boot - unsquashfs -f -d $TMPDIR $prep_results_dir/binary/live/filesystem.squashfs + unsquashfs -n -f -d $TMPDIR $prep_results_dir/binary/live/filesystem.squashfs boot + + mkdir $TMPDIR/root/images $TMPDIR/root/config + B3SUM=$(b3sum $prep_results_dir/binary/live/filesystem.squashfs | head -c 16) + cp $prep_results_dir/binary/live/filesystem.squashfs $TMPDIR/root/images/$B3SUM.rootfs + ln -rsf $TMPDIR/root/images/$B3SUM.rootfs $TMPDIR/root/config/current.rootfs + + mkdir -p $TMPDIR/next $TMPDIR/lower $TMPDIR/root/config/work $TMPDIR/root/config/overlay + mount $TMPDIR/root/config/current.rootfs $TMPDIR/lower + + mount -t overlay -o lowerdir=$TMPDIR/lower,workdir=$TMPDIR/root/config/work,upperdir=$TMPDIR/root/config/overlay overlay $TMPDIR/next if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then - sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt - rsync -a $base_dir/raspberrypi/img/ $TMPDIR/ + sed -i 's| boot=startos| boot=startos init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt + rsync -a $base_dir/raspberrypi/img/ $TMPDIR/next/ fi + umount $TMPDIR/next + umount $TMPDIR/lower + umount $TMPDIR/boot - umount $TMPDIR + umount $TMPDIR/root e2fsck -fy `partition_for ${OUTPUT_DEVICE} 2` resize2fs -M `partition_for ${OUTPUT_DEVICE} 2` diff --git a/image-recipe/prepare.sh b/image-recipe/prepare.sh index 1c6779608..c31f6ada0 100755 --- a/image-recipe/prepare.sh +++ b/image-recipe/prepare.sh @@ -21,4 +21,18 @@ apt-get install -yq \ dosfstools \ e2fsprogs \ squashfs-tools \ - rsync + rsync \ + b3sum +# TODO: remove when util-linux is released at v2.39.3 +apt-get install -yq \ + git \ + build-essential \ + crossbuild-essential-arm64 \ + crossbuild-essential-amd64 \ + automake \ + autoconf \ + gettext \ + libtool \ + pkg-config \ + autopoint \ + bison \ No newline at end of file diff --git a/image-recipe/raspberrypi/img/etc/fstab b/image-recipe/raspberrypi/img/etc/fstab index 816b32bcd..5f5164232 100644 --- a/image-recipe/raspberrypi/img/etc/fstab +++ b/image-recipe/raspberrypi/img/etc/fstab @@ -1,2 +1,2 @@ -/dev/mmcblk0p1 /boot vfat umask=0077 0 2 -/dev/mmcblk0p2 / ext4 defaults 0 1 +/dev/mmcblk0p1 /boot vfat umask=0077 0 2 +/dev/mmcblk0p2 / ext4 defaults 0 1 diff --git a/image-recipe/raspberrypi/img/usr/lib/startos/scripts/init_resize.sh b/image-recipe/raspberrypi/img/usr/lib/startos/scripts/init_resize.sh index 931649897..1fdca1c83 100755 --- a/image-recipe/raspberrypi/img/usr/lib/startos/scripts/init_resize.sh +++ b/image-recipe/raspberrypi/img/usr/lib/startos/scripts/init_resize.sh @@ -1,7 +1,7 @@ #!/bin/bash get_variables () { - ROOT_PART_DEV=$(findmnt / -o source -n) + ROOT_PART_DEV=$(findmnt /media/startos/root -o source -n) ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3) ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4) ROOT_DEV="/dev/${ROOT_DEV_NAME}" @@ -89,12 +89,12 @@ main () { resize2fs $ROOT_PART_DEV - if ! systemd-machine-id-setup; then + if ! systemd-machine-id-setup --root=/media/startos/config/overlay/; then FAIL_REASON="systemd-machine-id-setup failed" return 1 fi - if ! ssh-keygen -A; then + if ! (mkdir -p /media/startos/config/overlay/etc/ssh && ssh-keygen -A -f /media/startos/config/overlay/); then FAIL_REASON="ssh host key generation failed" return 1 fi @@ -104,9 +104,6 @@ main () { return 0 } -mount -t proc proc /proc -mount -t sysfs sys /sys -mount -t tmpfs tmp /run mkdir -p /run/systemd mount /boot mount / -o remount,ro @@ -114,7 +111,7 @@ mount / -o remount,ro beep if main; then - sed -i 's| init=/usr/lib/startos/scripts/init_resize\.sh| boot=embassy|' /boot/cmdline.txt + sed -i 's| init=/usr/lib/startos/scripts/init_resize\.sh||' /boot/cmdline.txt echo "Resized root filesystem. Rebooting in 5 seconds..." sleep 5 else diff --git a/image-recipe/raspberrypi/squashfs/boot/cmdline.txt b/image-recipe/raspberrypi/squashfs/boot/cmdline.txt index 02de34729..315dc67a7 100644 --- a/image-recipe/raspberrypi/squashfs/boot/cmdline.txt +++ b/image-recipe/raspberrypi/squashfs/boot/cmdline.txt @@ -1 +1 @@ -usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=embassy \ No newline at end of file +usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=startos \ No newline at end of file diff --git a/image-recipe/raspberrypi/squashfs/boot/config.txt b/image-recipe/raspberrypi/squashfs/boot/config.txt index 17bd5dc4e..4e1962a65 100644 --- a/image-recipe/raspberrypi/squashfs/boot/config.txt +++ b/image-recipe/raspberrypi/squashfs/boot/config.txt @@ -83,4 +83,5 @@ arm_boost=1 [all] gpu_mem=16 dtoverlay=pwm-2chan,disable-bt -initramfs initrd.img-6.1.21-v8+ + +auto_initramfs=1 \ No newline at end of file diff --git a/image-recipe/raspberrypi/squashfs/etc/embassy/config.yaml b/image-recipe/raspberrypi/squashfs/etc/startos/config.yaml similarity index 100% rename from image-recipe/raspberrypi/squashfs/etc/embassy/config.yaml rename to image-recipe/raspberrypi/squashfs/etc/startos/config.yaml diff --git a/patch-db b/patch-db index 6af2221ad..0df18c651 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 6af2221add56f0a557b37a268ef9fb2299a05255 +Subproject commit 0df18c651f2311e2e26f3e6c8535a9e40b71502f diff --git a/sdk/.gitignore b/sdk/.gitignore new file mode 100644 index 000000000..1ac0f02e6 --- /dev/null +++ b/sdk/.gitignore @@ -0,0 +1,6 @@ +dist/ +baseDist/ +base/lib/coverage +base/lib/node_modules +package/lib/coverage +package/lib/node_modules \ No newline at end of file diff --git a/sdk/.prettierignore b/sdk/.prettierignore new file mode 100644 index 000000000..149281301 --- /dev/null +++ b/sdk/.prettierignore @@ -0,0 +1 @@ +/base/lib/exver/exver.ts \ No newline at end of file diff --git a/sdk/Makefile b/sdk/Makefile new file mode 100644 index 000000000..3f8ae533a --- /dev/null +++ b/sdk/Makefile @@ -0,0 +1,74 @@ +PACKAGE_TS_FILES := $(shell git ls-files package/lib) package/lib/test/output.ts +BASE_TS_FILES := $(shell git ls-files base/lib) package/lib/test/output.ts +version = $(shell git tag --sort=committerdate | tail -1) + +.PHONY: test base/test package/test clean bundle fmt buildOutput check + +all: bundle + +package/test: $(PACKAGE_TS_FILES) package/lib/test/output.ts package/node_modules base/node_modules + cd package && npm test + +base/test: $(BASE_TS_FILES) base/node_modules + cd base && npm test + +test: base/test package/test + +clean: + rm -rf base/node_modules + rm -rf dist + rm -rf baseDist + rm -f package/lib/test/output.ts + rm -rf package/node_modules + +package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts + cd package && npm run buildOutput + +bundle: baseDist dist | test fmt + touch dist + +base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs + cd base && npm run peggy + +baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE + (cd base && npm run tsc) + rsync -ac base/node_modules baseDist/ + cp base/package.json baseDist/package.json + cp base/README.md baseDist/README.md + cp base/LICENSE baseDist/LICENSE + touch baseDist + +dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules package/README.md package/LICENSE + (cd package && npm run tsc) + rsync -ac package/node_modules dist/ + cp package/.npmignore dist/.npmignore + cp package/package.json dist/package.json + cp package/README.md dist/README.md + cp package/LICENSE dist/LICENSE + touch dist + +full-bundle: bundle + +check: + cd package + npm run check + cd ../base + npm run check + +fmt: package/node_modules base/node_modules + npx prettier . "**/*.ts" --write + + +package/node_modules: package/package.json + cd package && npm ci + +base/node_modules: base/package.json + cd base && npm ci + +node_modules: package/node_modules base/node_modules + +publish: bundle package/package.json package/README.md package/LICENSE + cd dist && npm publish --access=public + +link: bundle + cd dist && npm link diff --git a/sdk/base/.gitignore b/sdk/base/.gitignore new file mode 100644 index 000000000..a7ca92b2d --- /dev/null +++ b/sdk/base/.gitignore @@ -0,0 +1,5 @@ +.vscode +dist/ +node_modules/ +lib/coverage +lib/test/output.ts \ No newline at end of file diff --git a/sdk/base/LICENSE b/sdk/base/LICENSE new file mode 100644 index 000000000..793257b96 --- /dev/null +++ b/sdk/base/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Start9 Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/base/README.md b/sdk/base/README.md new file mode 100644 index 000000000..33bf5ff9d --- /dev/null +++ b/sdk/base/README.md @@ -0,0 +1 @@ +# See ../package/README.md diff --git a/sdk/base/jest.config.js b/sdk/base/jest.config.js new file mode 100644 index 000000000..c38fa5062 --- /dev/null +++ b/sdk/base/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + automock: false, + testEnvironment: "node", + rootDir: "./lib/", + modulePathIgnorePatterns: ["./dist/"], +} diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts new file mode 100644 index 000000000..dcb03af4e --- /dev/null +++ b/sdk/base/lib/Effects.ts @@ -0,0 +1,190 @@ +import { + ActionId, + ActionInput, + ActionMetadata, + SetMainStatus, + DependencyRequirement, + CheckDependenciesResult, + SetHealth, + BindParams, + HostId, + NetInfo, + Host, + ExportServiceInterfaceParams, + ServiceInterface, + RequestActionParams, + MainStatus, +} from "./osBindings" +import { StorePath } from "./util/PathBuilder" +import { + PackageId, + Dependencies, + ServiceInterfaceId, + SmtpValue, + ActionResult, +} from "./types" +import { UrlString } from "./util/getServiceInterface" + +/** Used to reach out from the pure js runtime */ + +export type Effects = { + constRetry: () => void + clearCallbacks: ( + options: { only: number[] } | { except: number[] }, + ) => Promise + + // action + action: { + /** Define an action that can be invoked by a user or service */ + export(options: { id: ActionId; metadata: ActionMetadata }): Promise + /** Remove all exported actions */ + clear(options: { except: ActionId[] }): Promise + getInput(options: { + packageId?: PackageId + actionId: ActionId + }): Promise + run>(options: { + packageId?: PackageId + actionId: ActionId + input?: Input + }): Promise + request>( + options: RequestActionParams, + ): Promise + clearRequests( + options: { only: string[] } | { except: string[] }, + ): Promise + } + + // control + /** restart this service's main function */ + restart(): Promise + /** stop this service's main function */ + shutdown(): Promise + /** ask the host os what the service's current status is */ + getStatus(options: { + packageId?: PackageId + callback?: () => void + }): Promise + /** indicate to the host os what runstate the service is in */ + setMainStatus(options: SetMainStatus): Promise + + // dependency + /** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */ + setDependencies(options: { dependencies: Dependencies }): Promise + /** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */ + getDependencies(): Promise + /** Test whether current dependency requirements are satisfied */ + checkDependencies(options: { + packageIds?: PackageId[] + }): Promise + /** mount a volume of a dependency */ + mount(options: { + location: string + target: { + packageId: string + volumeId: string + subpath: string | null + readonly: boolean + } + }): Promise + /** Returns a list of the ids of all installed packages */ + getInstalledPackages(): Promise + /** grants access to certain paths in the store to dependents */ + exposeForDependents(options: { paths: string[] }): Promise + + // health + /** sets the result of a health check */ + setHealth(o: SetHealth): Promise + + // subcontainer + subcontainer: { + /** A low level api used by SubContainer */ + createFs(options: { + imageId: string + name: string | null + }): Promise<[string, string]> + /** A low level api used by SubContainer */ + destroyFs(options: { guid: string }): Promise + } + + // net + // bind + /** Creates a host connected to the specified port with the provided options */ + bind(options: BindParams): Promise + /** Get the port address for a service */ + getServicePortForward(options: { + packageId?: PackageId + hostId: HostId + internalPort: number + }): Promise + /** Removes all network bindings, called in the setupInputSpec */ + clearBindings(options: { + except: { id: HostId; internalPort: number }[] + }): Promise + // host + /** Returns information about the specified host, if it exists */ + getHostInfo(options: { + packageId?: PackageId + hostId: HostId + callback?: () => void + }): Promise + /** Returns the IP address of the container */ + getContainerIp(): Promise + // interface + /** Creates an interface bound to a specific host and port to show to the user */ + exportServiceInterface(options: ExportServiceInterfaceParams): Promise + /** Returns an exported service interface */ + getServiceInterface(options: { + packageId?: PackageId + serviceInterfaceId: ServiceInterfaceId + callback?: () => void + }): Promise + /** Returns all exported service interfaces for a package */ + listServiceInterfaces(options: { + packageId?: PackageId + callback?: () => void + }): Promise> + /** Removes all service interfaces */ + clearServiceInterfaces(options: { + except: ServiceInterfaceId[] + }): Promise + // ssl + /** Returns a PEM encoded fullchain for the hostnames specified */ + getSslCertificate: (options: { + hostnames: string[] + algorithm?: "ecdsa" | "ed25519" + callback?: () => void + }) => Promise<[string, string, string]> + /** Returns a PEM encoded private key corresponding to the certificate for the hostnames specified */ + getSslKey: (options: { + hostnames: string[] + algorithm?: "ecdsa" | "ed25519" + }) => Promise + + // store + store: { + /** Get a value in a json like data, can be observed and subscribed */ + get(options: { + /** If there is no packageId it is assumed the current package */ + packageId?: string + /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ + path: StorePath + callback?: () => void + }): Promise + /** Used to store values that can be accessed and subscribed to */ + set(options: { + /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ + path: StorePath + value: ExtractStore + }): Promise + } + /** sets the version that this service's data has been migrated to */ + setDataVersion(options: { version: string }): Promise + /** returns the version that this service's data has been migrated to */ + getDataVersion(): Promise + + // system + /** Returns globally configured SMTP settings, if they exist */ + getSystemSmtp(options: { callback?: () => void }): Promise +} diff --git a/sdk/base/lib/actions/index.ts b/sdk/base/lib/actions/index.ts new file mode 100644 index 000000000..4bcbec8b1 --- /dev/null +++ b/sdk/base/lib/actions/index.ts @@ -0,0 +1,100 @@ +import * as T from "../types" +import * as IST from "../actions/input/inputSpecTypes" +import { Action } from "./setupActions" +import { ExtractInputSpecType } from "./input/builder/inputSpec" + +export type RunActionInput = + | Input + | ((prev?: { spec: IST.InputSpec; value: Input | null }) => Input) + +export const runAction = async < + Input extends Record, +>(options: { + effects: T.Effects + // packageId?: T.PackageId + actionId: T.ActionId + input?: RunActionInput +}) => { + if (options.input) { + if (options.input instanceof Function) { + const prev = await options.effects.action.getInput({ + // packageId: options.packageId, + actionId: options.actionId, + }) + const input = options.input( + prev + ? { spec: prev.spec as IST.InputSpec, value: prev.value as Input } + : undefined, + ) + return options.effects.action.run({ + // packageId: options.packageId, + actionId: options.actionId, + input, + }) + } else { + return options.effects.action.run({ + // packageId: options.packageId, + actionId: options.actionId, + input: options.input, + }) + } + } else { + return options.effects.action.run({ + // packageId: options.packageId, + actionId: options.actionId, + }) + } +} +type GetActionInputType> = + A extends Action ? ExtractInputSpecType : never + +type ActionRequestBase = { + reason?: string + replayId?: string +} +type ActionRequestInput> = { + kind: "partial" + value: Partial> +} +export type ActionRequestOptions> = + ActionRequestBase & + ( + | { + when?: Exclude< + T.ActionRequestTrigger, + { condition: "input-not-matches" } + > + input?: ActionRequestInput + } + | { + when: T.ActionRequestTrigger & { condition: "input-not-matches" } + input: ActionRequestInput + } + ) + +const _validate: T.ActionRequest = {} as ActionRequestOptions & { + actionId: string + packageId: string + severity: T.ActionSeverity +} + +export const requestAction = >(options: { + effects: T.Effects + packageId: T.PackageId + action: T + severity: T.ActionSeverity + options?: ActionRequestOptions +}) => { + const request = options.options || {} + const actionId = options.action.id + const req = { + ...request, + actionId, + packageId: options.packageId, + action: undefined, + severity: options.severity, + replayId: request.replayId || `${options.packageId}:${actionId}`, + } + delete req.action + return options.effects.action.request(req) +} diff --git a/sdk/base/lib/actions/input/builder/index.ts b/sdk/base/lib/actions/input/builder/index.ts new file mode 100644 index 000000000..618c4856f --- /dev/null +++ b/sdk/base/lib/actions/input/builder/index.ts @@ -0,0 +1,6 @@ +import { InputSpec } from "./inputSpec" +import { List } from "./list" +import { Value } from "./value" +import { Variants } from "./variants" + +export { InputSpec as InputSpec, List, Value, Variants } diff --git a/sdk/base/lib/actions/input/builder/inputSpec.ts b/sdk/base/lib/actions/input/builder/inputSpec.ts new file mode 100644 index 000000000..5d4d5c6bb --- /dev/null +++ b/sdk/base/lib/actions/input/builder/inputSpec.ts @@ -0,0 +1,149 @@ +import { ValueSpec } from "../inputSpecTypes" +import { Value } from "./value" +import { _ } from "../../../util" +import { Effects } from "../../../Effects" +import { Parser, object } from "ts-matches" +import { DeepPartial } from "../../../types" + +export type LazyBuildOptions = { + effects: Effects +} +export type LazyBuild = ( + options: LazyBuildOptions, +) => Promise | ExpectedOut + +// prettier-ignore +export type ExtractInputSpecType | InputSpec, any> | InputSpec, never>> = + A extends InputSpec | InputSpec ? B : + A + +export type ExtractPartialInputSpecType< + A extends + | Record + | InputSpec, any> + | InputSpec, never>, +> = A extends InputSpec | InputSpec + ? DeepPartial + : DeepPartial + +export type InputSpecOf, Store = never> = { + [K in keyof A]: Value +} + +export type MaybeLazyValues = LazyBuild | A +/** + * InputSpecs are the specs that are used by the os input specification form for this service. + * Here is an example of a simple input specification + ```ts + const smallInputSpec = InputSpec.of({ + test: Value.boolean({ + name: "Test", + description: "This is the description for the test", + warning: null, + default: false, + }), + }); + ``` + + The idea of an inputSpec is that now the form is going to ask for + Test: [ ] and the value is going to be checked as a boolean. + There are more complex values like selects, lists, and objects. See {@link Value} + + Also, there is the ability to get a validator/parser from this inputSpec spec. + ```ts + const matchSmallInputSpec = smallInputSpec.validator(); + type SmallInputSpec = typeof matchSmallInputSpec._TYPE; + ``` + + Here is an example of a more complex input specification which came from an input specification for a service + that works with bitcoin, like c-lightning. + ```ts + + export const hostname = Value.string({ + name: "Hostname", + default: null, + description: "Domain or IP address of bitcoin peer", + warning: null, + required: true, + masked: false, + placeholder: null, + pattern: + "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + patternDescription: + "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", +}); +export const port = Value.number({ + name: "Port", + default: null, + description: "Port that peer is listening on for inbound p2p connections", + warning: null, + required: false, + range: "[0,65535]", + integral: true, + units: null, + placeholder: null, +}); +export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port }); + + ``` + */ +export class InputSpec, Store = never> { + private constructor( + private readonly spec: { + [K in keyof Type]: Value | Value + }, + public validator: Parser, + ) {} + public _TYPE: Type = null as any as Type + public _PARTIAL: DeepPartial = null as any as DeepPartial + async build(options: LazyBuildOptions) { + const answer = {} as { + [K in keyof Type]: ValueSpec + } + for (const k in this.spec) { + answer[k] = await this.spec[k].build(options as any) + } + return answer + } + + static of< + Spec extends Record | Value>, + Store = never, + >(spec: Spec) { + const validatorObj = {} as { + [K in keyof Spec]: Parser + } + for (const key in spec) { + validatorObj[key] = spec[key].validator + } + const validator = object(validatorObj) + return new InputSpec< + { + [K in keyof Spec]: Spec[K] extends + | Value + | Value + ? T + : never + }, + Store + >(spec, validator as any) + } + + /** + * Use this during the times that the input needs a more specific type. + * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. + ```ts + const a = InputSpec.text({ + name: "a", + required: false, + }) + + return InputSpec.of()({ + myValue: a.withStore(), + }) + ``` + */ + withStore() { + return this as any as InputSpec + } +} diff --git a/sdk/base/lib/actions/input/builder/list.ts b/sdk/base/lib/actions/input/builder/list.ts new file mode 100644 index 000000000..726dc961e --- /dev/null +++ b/sdk/base/lib/actions/input/builder/list.ts @@ -0,0 +1,198 @@ +import { InputSpec, LazyBuild } from "./inputSpec" +import { + ListValueSpecText, + Pattern, + RandomString, + UniqueBy, + ValueSpecList, + ValueSpecListOf, +} from "../inputSpecTypes" +import { Parser, arrayOf, string } from "ts-matches" + +export class List { + private constructor( + public build: LazyBuild, + public validator: Parser, + ) {} + + static text( + a: { + name: string + description?: string | null + warning?: string | null + default?: string[] + minLength?: number | null + maxLength?: number | null + }, + aSpec: { + /** + * @description Mask (aka camouflage) text input with dots: ● ● ● + * @default false + */ + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + /** + * @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails. + * @default [] + * @example + * ``` + [ + { + regex: "[a-z]", + description: "May only contain lower case letters from the English alphabet." + } + ] + * ``` + */ + patterns?: Pattern[] + /** + * @description Informs the browser how to behave and which keyboard to display on mobile + * @default "text" + */ + inputmode?: ListValueSpecText["inputmode"] + /** + * @description Displays a button that will generate a random string according to the provided charset and len attributes. + */ + generate?: null | RandomString + }, + ) { + return new List(() => { + const spec = { + type: "text" as const, + placeholder: null, + minLength: null, + maxLength: null, + masked: false, + inputmode: "text" as const, + generate: null, + patterns: aSpec.patterns || [], + ...aSpec, + } + const built: ValueSpecListOf<"text"> = { + description: null, + warning: null, + default: [], + type: "list" as const, + minLength: null, + maxLength: null, + disabled: false, + ...a, + spec, + } + return built + }, arrayOf(string)) + } + + static dynamicText( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + generate?: null | RandomString + spec: { + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ListValueSpecText["inputmode"] + } + } + >, + ) { + return new List(async (options) => { + const { spec: aSpec, ...a } = await getA(options) + const spec = { + type: "text" as const, + placeholder: null, + minLength: null, + maxLength: null, + masked: false, + inputmode: "text" as const, + generate: null, + patterns: aSpec.patterns || [], + ...aSpec, + } + const built: ValueSpecListOf<"text"> = { + description: null, + warning: null, + default: [], + type: "list" as const, + minLength: null, + maxLength: null, + disabled: false, + ...a, + spec, + } + return built + }, arrayOf(string)) + } + + static obj, Store>( + a: { + name: string + description?: string | null + warning?: string | null + default?: [] + minLength?: number | null + maxLength?: number | null + }, + aSpec: { + spec: InputSpec + displayAs?: null | string + uniqueBy?: null | UniqueBy + }, + ) { + return new List(async (options) => { + const { spec: previousSpecSpec, ...restSpec } = aSpec + const specSpec = await previousSpecSpec.build(options) + const spec = { + type: "object" as const, + displayAs: null, + uniqueBy: null, + ...restSpec, + spec: specSpec, + } + const value = { + spec, + default: [], + ...a, + } + return { + description: null, + warning: null, + minLength: null, + maxLength: null, + type: "list" as const, + disabled: false, + ...value, + } + }, arrayOf(aSpec.spec.validator)) + } + + /** + * Use this during the times that the input needs a more specific type. + * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. + ```ts + const a = InputSpec.text({ + name: "a", + required: false, + }) + + return InputSpec.of()({ + myValue: a.withStore(), + }) + ``` + */ + withStore() { + return this as any as List + } +} diff --git a/sdk/base/lib/actions/input/builder/value.ts b/sdk/base/lib/actions/input/builder/value.ts new file mode 100644 index 000000000..3ff3c2d24 --- /dev/null +++ b/sdk/base/lib/actions/input/builder/value.ts @@ -0,0 +1,840 @@ +import { InputSpec, LazyBuild } from "./inputSpec" +import { List } from "./list" +import { Variants } from "./variants" +import { + FilePath, + Pattern, + RandomString, + ValueSpec, + ValueSpecDatetime, + ValueSpecHidden, + ValueSpecText, + ValueSpecTextarea, +} from "../inputSpecTypes" +import { DefaultString } from "../inputSpecTypes" +import { _, once } from "../../../util" +import { + Parser, + any, + anyOf, + arrayOf, + boolean, + literal, + literals, + number, + object, + string, + unknown, +} from "ts-matches" +import { DeepPartial } from "../../../types" + +type AsRequired = Required extends true + ? T + : T | null + +const testForAsRequiredParser = once( + () => object({ required: literal(true) }).test, +) +function asRequiredParser< + Type, + Input, + Return extends Parser | Parser, +>(parser: Parser, input: Input): Return { + if (testForAsRequiredParser()(input)) return parser as any + return parser.nullable() as any +} + +export class Value { + protected constructor( + public build: LazyBuild, + public validator: Parser, + ) {} + public _TYPE: Type = null as any as Type + public _PARTIAL: DeepPartial = null as any as DeepPartial + + static toggle(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + default: boolean + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value( + async () => ({ + description: null, + warning: null, + type: "toggle" as const, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + boolean, + ) + } + static dynamicToggle( + a: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: boolean + disabled?: false | string + } + >, + ) { + return new Value( + async (options) => ({ + description: null, + warning: null, + type: "toggle" as const, + disabled: false, + immutable: false, + ...(await a(options)), + }), + boolean, + ) + } + static text(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * provide a default value. + * @type { string | RandomString | null } + * @example default: null + * @example default: 'World' + * @example default: { charset: 'abcdefg', len: 16 } + */ + default: string | RandomString | null + required: Required + /** + * @description Mask (aka camouflage) text input with dots: ● ● ● + * @default false + */ + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + /** + * @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails. + * @default [] + * @example + * ``` + [ + { + regex: "[a-z]", + description: "May only contain lower case letters from the English alphabet." + } + ] + * ``` + */ + patterns?: Pattern[] + /** + * @description Informs the browser how to behave and which keyboard to display on mobile + * @default "text" + */ + inputmode?: ValueSpecText["inputmode"] + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + /** + * @description Displays a button that will generate a random string according to the provided charset and len attributes. + */ + generate?: RandomString | null + }) { + return new Value, never>( + async () => ({ + type: "text" as const, + description: null, + warning: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: null, + patterns: [], + inputmode: "text", + disabled: false, + immutable: a.immutable ?? false, + generate: a.generate ?? null, + ...a, + }), + asRequiredParser(string, a), + ) + } + static dynamicText( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: DefaultString | null + required: boolean + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ValueSpecText["inputmode"] + disabled?: string | false + generate?: null | RandomString + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + type: "text" as const, + description: null, + warning: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: null, + patterns: [], + inputmode: "text", + disabled: false, + immutable: false, + generate: a.generate ?? null, + ...a, + } + }, string.nullable()) + } + static textarea(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + default: string | null + required: Required + minLength?: number | null + maxLength?: number | null + placeholder?: string | null + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value, never>( + async () => { + const built: ValueSpecTextarea = { + description: null, + warning: null, + minLength: null, + maxLength: null, + placeholder: null, + type: "textarea" as const, + disabled: false, + immutable: a.immutable ?? false, + ...a, + } + return built + }, + asRequiredParser(string, a), + ) + } + static dynamicTextarea( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + minLength?: number | null + maxLength?: number | null + placeholder?: string | null + disabled?: false | string + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + description: null, + warning: null, + minLength: null, + maxLength: null, + placeholder: null, + type: "textarea" as const, + disabled: false, + immutable: false, + ...a, + } + }, string.nullable()) + } + static number(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { default: number | null } + * @example default: null + * @example default: 7 + */ + default: number | null + required: Required + min?: number | null + max?: number | null + /** + * @description How much does the number increase/decrease when using the arrows provided by the browser. + * @default 1 + */ + step?: number | null + /** + * @description Requires the number to be an integer. + */ + integer: boolean + /** + * @description Optionally display units to the right of the input box. + */ + units?: string | null + placeholder?: string | null + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value, never>( + () => ({ + type: "number" as const, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + asRequiredParser(number, a), + ) + } + static dynamicNumber( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: number | null + required: boolean + min?: number | null + max?: number | null + step?: number | null + integer: boolean + units?: string | null + placeholder?: string | null + disabled?: false | string + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + type: "number" as const, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + disabled: false, + immutable: false, + ...a, + } + }, number.nullable()) + } + static color(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { default: string | null } + * @example default: null + * @example default: 'ffffff' + */ + default: string | null + required: Required + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value, never>( + () => ({ + type: "color" as const, + description: null, + warning: null, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + asRequiredParser(string, a), + ) + } + + static dynamicColor( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + disabled?: false | string + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + type: "color" as const, + description: null, + warning: null, + disabled: false, + immutable: false, + ...a, + } + }, string.nullable()) + } + static datetime(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { default: string | null } + * @example default: null + * @example default: '1985-12-16 18:00:00.000' + */ + default: string | null + required: Required + /** + * @description Informs the browser how to behave and which date/time component to display. + * @default "datetime-local" + */ + inputmode?: ValueSpecDatetime["inputmode"] + min?: string | null + max?: string | null + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value, never>( + () => ({ + type: "datetime" as const, + description: null, + warning: null, + inputmode: "datetime-local", + min: null, + max: null, + step: null, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + asRequiredParser(string, a), + ) + } + static dynamicDatetime( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: boolean + inputmode?: ValueSpecDatetime["inputmode"] + min?: string | null + max?: string | null + disabled?: false | string + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + type: "datetime" as const, + description: null, + warning: null, + inputmode: "datetime-local", + min: null, + max: null, + disabled: false, + immutable: false, + ...a, + } + }, string.nullable()) + } + static select>(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description Determines if the field is required. If so, optionally provide a default value from the list of values. + * @type { (keyof Values & string) | null } + * @example default: null + * @example default: 'radio1' + */ + default: keyof Values & string + /** + * @description A mapping of unique radio options to their human readable display format. + * @example + * ``` + { + radio1: "Radio 1" + radio2: "Radio 2" + radio3: "Radio 3" + } + * ``` + */ + values: Values + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value( + () => ({ + description: null, + warning: null, + type: "select" as const, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + anyOf( + ...Object.keys(a.values).map((x: keyof Values & string) => literal(x)), + ), + ) + } + static dynamicSelect( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: string + values: Record + disabled?: false | string | string[] + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + description: null, + warning: null, + type: "select" as const, + disabled: false, + immutable: false, + ...a, + } + }, string) + } + static multiselect>(a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description A simple list of which options should be checked by default. + */ + default: (keyof Values & string)[] + /** + * @description A mapping of checkbox options to their human readable display format. + * @example + * ``` + { + option1: "Option 1" + option2: "Option 2" + option3: "Option 3" + } + * ``` + */ + values: Values + minLength?: number | null + maxLength?: number | null + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }) { + return new Value<(keyof Values)[], never>( + () => ({ + type: "multiselect" as const, + minLength: null, + maxLength: null, + warning: null, + description: null, + disabled: false, + immutable: a.immutable ?? false, + ...a, + }), + arrayOf( + literals(...(Object.keys(a.values) as any as [keyof Values & string])), + ), + ) + } + static dynamicMultiselect( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: string[] + values: Record + minLength?: number | null + maxLength?: number | null + disabled?: false | string | string[] + } + >, + ) { + return new Value(async (options) => { + const a = await getA(options) + return { + type: "multiselect" as const, + minLength: null, + maxLength: null, + warning: null, + description: null, + disabled: false, + immutable: false, + ...a, + } + }, arrayOf(string)) + } + static object, Store>( + a: { + name: string + description?: string | null + }, + spec: InputSpec, + ) { + return new Value(async (options) => { + const built = await spec.build(options as any) + return { + type: "object" as const, + description: null, + warning: null, + ...a, + spec: built, + } + }, spec.validator) + } + // static file(a: { + // name: string + // description?: string | null + // extensions: string[] + // required: Required + // }) { + // const buildValue = { + // type: "file" as const, + // description: null, + // warning: null, + // ...a, + // } + // return new Value, Store>( + // () => ({ + // ...buildValue, + // }), + // asRequiredParser(object({ filePath: string }), a), + // ) + // } + // static dynamicFile( + // a: LazyBuild< + // Store, + // { + // name: string + // description?: string | null + // warning?: string | null + // extensions: string[] + // required: boolean + // } + // >, + // ) { + // return new Value( + // async (options) => ({ + // type: "file" as const, + // description: null, + // warning: null, + // ...(await a(options)), + // }), + // object({ filePath: string }).nullable(), + // ) + // } + static union< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + Store, + >( + a: { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description Provide a default value from the list of variants. + * @type { string } + * @example default: 'variant1' + */ + default: keyof VariantValues & string + /** + * @description Once set, the value can never be changed. + * @default false + */ + immutable?: boolean + }, + aVariants: Variants, + ) { + return new Value( + async (options) => ({ + type: "union" as const, + description: null, + warning: null, + disabled: false, + ...a, + variants: await aVariants.build(options as any), + immutable: a.immutable ?? false, + }), + aVariants.validator, + ) + } + static filteredUnion< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + Store, + >( + getDisabledFn: LazyBuild, + a: { + name: string + description?: string | null + warning?: string | null + default: keyof VariantValues & string + }, + aVariants: Variants | Variants, + ) { + return new Value( + async (options) => ({ + type: "union" as const, + description: null, + warning: null, + ...a, + variants: await aVariants.build(options as any), + disabled: (await getDisabledFn(options)) || false, + immutable: false, + }), + aVariants.validator, + ) + } + static dynamicUnion< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + Store, + >( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default: keyof VariantValues & string + disabled: string[] | false | string + } + >, + aVariants: Variants | Variants, + ) { + return new Value( + async (options) => { + const newValues = await getA(options) + return { + type: "union" as const, + description: null, + warning: null, + ...newValues, + variants: await aVariants.build(options as any), + immutable: false, + } + }, + aVariants.validator, + ) + } + + static list(a: List) { + return new Value((options) => a.build(options), a.validator) + } + + static hidden(parser: Parser = any) { + return new Value(async () => { + const built: ValueSpecHidden = { + type: "hidden" as const, + } + return built + }, parser) + } + + map(fn: (value: Type) => U): Value { + return new Value(this.build, this.validator.map(fn)) + } + + /** + * Use this during the times that the input needs a more specific type. + * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. + ```ts + const a = InputSpec.text({ + name: "a", + required: false, + }) + + return InputSpec.of()({ + myValue: a.withStore(), + }) + ``` + */ + withStore() { + return this as any as Value + } +} diff --git a/sdk/base/lib/actions/input/builder/variants.ts b/sdk/base/lib/actions/input/builder/variants.ts new file mode 100644 index 000000000..93453d73c --- /dev/null +++ b/sdk/base/lib/actions/input/builder/variants.ts @@ -0,0 +1,145 @@ +import { DeepPartial } from "../../../types" +import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes" +import { + LazyBuild, + InputSpec, + ExtractInputSpecType, + ExtractPartialInputSpecType, +} from "./inputSpec" +import { Parser, anyOf, literal, object } from "ts-matches" + +export type UnionRes< + Store, + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + K extends keyof VariantValues & string = keyof VariantValues & string, +> = { + [key in keyof VariantValues]: { + selection: key + value: ExtractInputSpecType + other?: { + [key2 in Exclude]?: DeepPartial< + ExtractInputSpecType + > + } + } +}[K] + +/** + * Used in the the Value.select { @link './value.ts' } + * to indicate the type of select variants that are available. The key for the record passed in will be the + * key to the tag.id in the Value.select +```ts + +export const disabled = InputSpec.of({}); +export const size = Value.number({ + name: "Max Chain Size", + default: 550, + description: "Limit of blockchain size on disk.", + warning: "Increasing this value will require re-syncing your node.", + required: true, + range: "[550,1000000)", + integral: true, + units: "MiB", + placeholder: null, +}); +export const automatic = InputSpec.of({ size: size }); +export const size1 = Value.number({ + name: "Failsafe Chain Size", + default: 65536, + description: "Prune blockchain if size expands beyond this.", + warning: null, + required: true, + range: "[550,1000000)", + integral: true, + units: "MiB", + placeholder: null, +}); +export const manual = InputSpec.of({ size: size1 }); +export const pruningSettingsVariants = Variants.of({ + disabled: { name: "Disabled", spec: disabled }, + automatic: { name: "Automatic", spec: automatic }, + manual: { name: "Manual", spec: manual }, +}); +export const pruning = Value.union( + { + name: "Pruning Settings", + description: + '- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n', + warning: null, + default: "disabled", + }, + pruningSettingsVariants +); +``` + */ +export class Variants< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + Store, +> { + private constructor( + public build: LazyBuild, + public validator: Parser>, + ) {} + static of< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + Store = never, + >(a: VariantValues) { + const validator = anyOf( + ...Object.entries(a).map(([id, { spec }]) => + object({ + selection: literal(id), + value: spec.validator, + }), + ), + ) as Parser + + return new Variants(async (options) => { + const variants = {} as { + [K in keyof VariantValues]: { + name: string + spec: Record + } + } + for (const key in a) { + const value = a[key] + variants[key] = { + name: value.name, + spec: await value.spec.build(options as any), + } + } + return variants + }, validator) + } + /** + * Use this during the times that the input needs a more specific type. + * Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else. + ```ts + const a = InputSpec.text({ + name: "a", + required: false, + }) + + return InputSpec.of()({ + myValue: a.withStore(), + }) + ``` + */ + withStore() { + return this as any as Variants + } +} diff --git a/sdk/base/lib/actions/input/index.ts b/sdk/base/lib/actions/input/index.ts new file mode 100644 index 000000000..3fc16f585 --- /dev/null +++ b/sdk/base/lib/actions/input/index.ts @@ -0,0 +1,3 @@ +export * as constants from "./inputSpecConstants" +export * as types from "./inputSpecTypes" +export * as builder from "./builder" diff --git a/sdk/base/lib/actions/input/inputSpecConstants.ts b/sdk/base/lib/actions/input/inputSpecConstants.ts new file mode 100644 index 000000000..64857d419 --- /dev/null +++ b/sdk/base/lib/actions/input/inputSpecConstants.ts @@ -0,0 +1,80 @@ +import { SmtpValue } from "../../types" +import { GetSystemSmtp, Patterns } from "../../util" +import { InputSpec, InputSpecOf } from "./builder/inputSpec" +import { Value } from "./builder/value" +import { Variants } from "./builder/variants" + +/** + * Base SMTP settings, to be used by StartOS for system wide SMTP + */ +export const customSmtp = InputSpec.of, never>({ + server: Value.text({ + name: "SMTP Server", + required: true, + default: null, + }), + port: Value.number({ + name: "Port", + required: true, + default: 587, + min: 1, + max: 65535, + integer: true, + }), + from: Value.text({ + name: "From Address", + required: true, + default: null, + placeholder: "Example Name ", + inputmode: "email", + patterns: [Patterns.emailWithName], + }), + login: Value.text({ + name: "Login", + required: true, + default: null, + }), + password: Value.text({ + name: "Password", + required: false, + default: null, + masked: true, + }), +}) + +/** + * For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings + */ +export const smtpInputSpec = Value.filteredUnion( + async ({ effects }) => { + const smtp = await new GetSystemSmtp(effects).once() + return smtp ? [] : ["system"] + }, + { + name: "SMTP", + description: "Optionally provide an SMTP server for sending emails", + default: "disabled", + }, + Variants.of({ + disabled: { name: "Disabled", spec: InputSpec.of({}) }, + system: { + name: "System Credentials", + spec: InputSpec.of({ + customFrom: Value.text({ + name: "Custom From Address", + description: + "A custom from address for this service. If not provided, the system from address will be used.", + required: false, + default: null, + placeholder: "test@example.com", + inputmode: "email", + patterns: [Patterns.email], + }), + }), + }, + custom: { + name: "Custom Credentials", + spec: customSmtp, + }, + }), +) diff --git a/sdk/base/lib/actions/input/inputSpecTypes.ts b/sdk/base/lib/actions/input/inputSpecTypes.ts new file mode 100644 index 000000000..362a56ea1 --- /dev/null +++ b/sdk/base/lib/actions/input/inputSpecTypes.ts @@ -0,0 +1,249 @@ +export type InputSpec = Record +export type ValueType = + | "text" + | "textarea" + | "number" + | "color" + | "datetime" + | "toggle" + | "select" + | "multiselect" + | "list" + | "object" + | "file" + | "union" + | "hidden" +export type ValueSpec = ValueSpecOf +/** core spec types. These types provide the metadata for performing validations */ +// prettier-ignore +export type ValueSpecOf = + T extends "text" ? ValueSpecText : + T extends "textarea" ? ValueSpecTextarea : + T extends "number" ? ValueSpecNumber : + T extends "color" ? ValueSpecColor : + T extends "datetime" ? ValueSpecDatetime : + T extends "toggle" ? ValueSpecToggle : + T extends "select" ? ValueSpecSelect : + T extends "multiselect" ? ValueSpecMultiselect : + T extends "list" ? ValueSpecList : + T extends "object" ? ValueSpecObject : + T extends "file" ? ValueSpecFile : + T extends "union" ? ValueSpecUnion : + T extends "hidden" ? ValueSpecHidden : + never + +export type ValueSpecText = { + name: string + description: string | null + warning: string | null + + type: "text" + patterns: Pattern[] + minLength: number | null + maxLength: number | null + masked: boolean + + inputmode: "text" | "email" | "tel" | "url" + placeholder: string | null + + required: boolean + default: DefaultString | null + disabled: false | string + generate: null | RandomString + immutable: boolean +} +export type ValueSpecTextarea = { + name: string + description: string | null + warning: string | null + + type: "textarea" + placeholder: string | null + minLength: number | null + maxLength: number | null + required: boolean + disabled: false | string + immutable: boolean +} + +export type FilePath = { + filePath: string +} +export type ValueSpecNumber = { + type: "number" + min: number | null + max: number | null + integer: boolean + step: number | null + units: string | null + placeholder: string | null + name: string + description: string | null + warning: string | null + required: boolean + default: number | null + disabled: false | string + immutable: boolean +} +export type ValueSpecColor = { + name: string + description: string | null + warning: string | null + + type: "color" + required: boolean + default: string | null + disabled: false | string + immutable: boolean +} +export type ValueSpecDatetime = { + name: string + description: string | null + warning: string | null + type: "datetime" + required: boolean + inputmode: "date" | "time" | "datetime-local" + min: string | null + max: string | null + default: string | null + disabled: false | string + immutable: boolean +} +export type ValueSpecSelect = { + values: Record + name: string + description: string | null + warning: string | null + type: "select" + default: string | null + disabled: false | string | string[] + immutable: boolean +} +export type ValueSpecMultiselect = { + values: Record + + name: string + description: string | null + warning: string | null + + type: "multiselect" + minLength: number | null + maxLength: number | null + disabled: false | string | string[] + default: string[] + immutable: boolean +} +export type ValueSpecToggle = { + name: string + description: string | null + warning: string | null + + type: "toggle" + default: boolean | null + disabled: false | string + immutable: boolean +} +export type ValueSpecUnion = { + name: string + description: string | null + warning: string | null + + type: "union" + variants: Record< + string, + { + name: string + spec: InputSpec + } + > + disabled: false | string | string[] + default: string | null + immutable: boolean +} +export type ValueSpecFile = { + name: string + description: string | null + warning: string | null + type: "file" + extensions: string[] + required: boolean +} +export type ValueSpecObject = { + name: string + description: string | null + warning: string | null + type: "object" + spec: InputSpec +} +export type ValueSpecHidden = { + type: "hidden" +} +export type ListValueSpecType = "text" | "object" +// prettier-ignore +export type ListValueSpecOf = + T extends "text" ? ListValueSpecText : + T extends "object" ? ListValueSpecObject : + never +export type ValueSpecList = ValueSpecListOf +export type ValueSpecListOf = { + name: string + description: string | null + warning: string | null + type: "list" + spec: ListValueSpecOf + minLength: number | null + maxLength: number | null + disabled: false | string + default: + | string[] + | DefaultString[] + | Record[] + | readonly string[] + | readonly DefaultString[] + | readonly Record[] +} +export type Pattern = { + regex: string + description: string +} +export type ListValueSpecText = { + type: "text" + patterns: Pattern[] + minLength: number | null + maxLength: number | null + masked: boolean + + generate: null | RandomString + inputmode: "text" | "email" | "tel" | "url" + placeholder: string | null +} +export type ListValueSpecObject = { + type: "object" + spec: InputSpec + uniqueBy: UniqueBy + displayAs: string | null +} +// TODO Aiden do we really want this expressivity? Why not the below. Also what's with the "readonly" portion? +// export type UniqueBy = null | string | { any: string[] } | { all: string[] } + +export type UniqueBy = + | null + | string + | { + any: readonly UniqueBy[] | UniqueBy[] + } + | { + all: readonly UniqueBy[] | UniqueBy[] + } +export type DefaultString = string | RandomString +export type RandomString = { + charset: string + len: number +} +// sometimes the type checker needs just a little bit of help +export function isValueSpecListOf( + t: ValueSpec, + s: S, +): t is ValueSpecListOf & { spec: ListValueSpecOf } { + return "spec" in t && t.spec.type === s +} diff --git a/sdk/base/lib/actions/setupActions.ts b/sdk/base/lib/actions/setupActions.ts new file mode 100644 index 000000000..203f81a32 --- /dev/null +++ b/sdk/base/lib/actions/setupActions.ts @@ -0,0 +1,155 @@ +import { InputSpec } from "./input/builder" +import { + ExtractInputSpecType, + ExtractPartialInputSpecType, +} from "./input/builder/inputSpec" +import * as T from "../types" +import { once } from "../util" + +export type Run< + A extends + | Record + | InputSpec, any> + | InputSpec, never>, +> = (options: { + effects: T.Effects + input: ExtractInputSpecType & Record +}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined> +export type GetInput< + A extends + | Record + | InputSpec, any> + | InputSpec, never>, +> = (options: { + effects: T.Effects +}) => Promise< + | null + | void + | undefined + | (ExtractPartialInputSpecType & Record) +> + +export type MaybeFn = T | ((options: { effects: T.Effects }) => Promise) +function callMaybeFn( + maybeFn: MaybeFn, + options: { effects: T.Effects }, +): Promise { + if (maybeFn instanceof Function) { + return maybeFn(options) + } else { + return Promise.resolve(maybeFn) + } +} +function mapMaybeFn( + maybeFn: MaybeFn, + map: (value: T) => U, +): MaybeFn { + if (maybeFn instanceof Function) { + return async (...args) => map(await maybeFn(...args)) + } else { + return map(maybeFn) + } +} + +export class Action< + Id extends T.ActionId, + Store, + InputSpecType extends + | Record + | InputSpec + | InputSpec, +> { + private constructor( + readonly id: Id, + private readonly metadataFn: MaybeFn, + private readonly inputSpec: InputSpecType, + private readonly getInputFn: GetInput>, + private readonly runFn: Run>, + ) {} + static withInput< + Id extends T.ActionId, + Store, + InputSpecType extends + | Record + | InputSpec + | InputSpec, + >( + id: Id, + metadata: MaybeFn>, + inputSpec: InputSpecType, + getInput: GetInput>, + run: Run>, + ): Action { + return new Action( + id, + mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), + inputSpec, + getInput, + run, + ) + } + static withoutInput( + id: Id, + metadata: MaybeFn>, + run: Run<{}>, + ): Action { + return new Action( + id, + mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), + {}, + async () => null, + run, + ) + } + async exportMetadata(options: { + effects: T.Effects + }): Promise { + const metadata = await callMaybeFn(this.metadataFn, options) + await options.effects.action.export({ id: this.id, metadata }) + return metadata + } + async getInput(options: { effects: T.Effects }): Promise { + return { + spec: await this.inputSpec.build(options), + value: (await this.getInputFn(options)) || null, + } + } + async run(options: { + effects: T.Effects + input: ExtractInputSpecType + }): Promise { + return (await this.runFn(options)) || null + } +} + +export class Actions< + Store, + AllActions extends Record>, +> { + private constructor(private readonly actions: AllActions) {} + static of(): Actions { + return new Actions({}) + } + addAction>( + action: A, + ): Actions { + return new Actions({ ...this.actions, [action.id]: action }) + } + async update(options: { effects: T.Effects }): Promise { + options.effects = { + ...options.effects, + constRetry: once(() => { + this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem + }), + } + for (let action of Object.values(this.actions)) { + await action.exportMetadata(options) + } + await options.effects.action.clear({ except: Object.keys(this.actions) }) + + return null + } + get(actionId: Id): AllActions[Id] { + return this.actions[actionId] + } +} diff --git a/sdk/base/lib/backup/Backups.ts b/sdk/base/lib/backup/Backups.ts new file mode 100644 index 000000000..3e644014a --- /dev/null +++ b/sdk/base/lib/backup/Backups.ts @@ -0,0 +1,208 @@ +import * as T from "../types" +import * as child_process from "child_process" +import { asError } from "../util" + +export const DEFAULT_OPTIONS: T.SyncOptions = { + delete: true, + exclude: [], +} +export type BackupSync = { + dataPath: `/media/startos/volumes/${Volumes}/${string}` + backupPath: `/media/startos/backup/${string}` + options?: Partial + backupOptions?: Partial + restoreOptions?: Partial +} +/** + * This utility simplifies the volume backup process. + * ```ts + * export const { createBackup, restoreBackup } = Backups.volumes("main").build(); + * ``` + * + * Changing the options of the rsync, (ie excludes) use either + * ```ts + * Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build() + * // or + * Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build() + * ``` + * + * Using the more fine control, using the addSets for more control + * ```ts + * Backups.addSets({ + * srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP + * }, { + * srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}} + * ).build()q + * ``` + */ +export class Backups { + private constructor( + private options = DEFAULT_OPTIONS, + private restoreOptions: Partial = {}, + private backupOptions: Partial = {}, + private backupSet = [] as BackupSync[], + ) {} + + static withVolumes( + ...volumeNames: Array + ): Backups { + return Backups.withSyncs( + ...volumeNames.map((srcVolume) => ({ + dataPath: `/media/startos/volumes/${srcVolume}/` as const, + backupPath: `/media/startos/backup/${srcVolume}/` as const, + })), + ) + } + + static withSyncs( + ...syncs: BackupSync[] + ) { + return syncs.reduce((acc, x) => acc.addSync(x), new Backups()) + } + + static withOptions( + options?: Partial, + ) { + return new Backups({ ...DEFAULT_OPTIONS, ...options }) + } + + setOptions(options?: Partial) { + this.options = { + ...this.options, + ...options, + } + return this + } + + setBackupOptions(options?: Partial) { + this.backupOptions = { + ...this.backupOptions, + ...options, + } + return this + } + + setRestoreOptions(options?: Partial) { + this.restoreOptions = { + ...this.restoreOptions, + ...options, + } + return this + } + + addVolume( + volume: M["volumes"][number], + options?: Partial<{ + options: T.SyncOptions + backupOptions: T.SyncOptions + restoreOptions: T.SyncOptions + }>, + ) { + return this.addSync({ + dataPath: `/media/startos/volumes/${volume}/` as const, + backupPath: `/media/startos/backup/${volume}/` as const, + ...options, + }) + } + addSync(sync: BackupSync) { + this.backupSet.push({ + ...sync, + options: { ...this.options, ...sync.options }, + }) + return this + } + + async createBackup() { + for (const item of this.backupSet) { + const rsyncResults = await runRsync({ + srcPath: item.dataPath, + dstPath: item.backupPath, + options: { + ...this.options, + ...this.backupOptions, + ...item.options, + ...item.backupOptions, + }, + }) + await rsyncResults.wait() + } + return + } + + async restoreBackup() { + for (const item of this.backupSet) { + const rsyncResults = await runRsync({ + srcPath: item.backupPath, + dstPath: item.dataPath, + options: { + ...this.options, + ...this.backupOptions, + ...item.options, + ...item.backupOptions, + }, + }) + await rsyncResults.wait() + } + return + } +} + +async function runRsync(rsyncOptions: { + srcPath: string + dstPath: string + options: T.SyncOptions +}): Promise<{ + id: () => Promise + wait: () => Promise + progress: () => Promise +}> { + const { srcPath, dstPath, options } = rsyncOptions + + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(srcPath) + args.push(dstPath) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(`Backups.runAsync`, asError(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code: any) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } +} diff --git a/sdk/base/lib/backup/setupBackups.ts b/sdk/base/lib/backup/setupBackups.ts new file mode 100644 index 000000000..b41a61f42 --- /dev/null +++ b/sdk/base/lib/backup/setupBackups.ts @@ -0,0 +1,39 @@ +import { Backups } from "./Backups" +import * as T from "../types" +import { _ } from "../util" + +export type SetupBackupsParams = + | M["volumes"][number][] + | ((_: { effects: T.Effects }) => Promise>) + +type SetupBackupsRes = { + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup +} + +export function setupBackups( + options: SetupBackupsParams, +) { + let backupsFactory: (_: { effects: T.Effects }) => Promise> + if (options instanceof Function) { + backupsFactory = options + } else { + backupsFactory = async () => Backups.withVolumes(...options) + } + const answer: { + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup + } = { + get createBackup() { + return (async (options) => { + return (await backupsFactory(options)).createBackup() + }) as T.ExpectedExports.createBackup + }, + get restoreBackup() { + return (async (options) => { + return (await backupsFactory(options)).restoreBackup() + }) as T.ExpectedExports.restoreBackup + }, + } + return answer +} diff --git a/sdk/base/lib/dependencies/dependencies.ts b/sdk/base/lib/dependencies/dependencies.ts new file mode 100644 index 000000000..20049b5e8 --- /dev/null +++ b/sdk/base/lib/dependencies/dependencies.ts @@ -0,0 +1,208 @@ +import { ExtendedVersion, VersionRange } from "../exver" +import { PackageId, HealthCheckId } from "../types" +import { Effects } from "../Effects" + +export type CheckDependencies = { + installedSatisfied: (packageId: DependencyId) => boolean + installedVersionSatisfied: (packageId: DependencyId) => boolean + runningSatisfied: (packageId: DependencyId) => boolean + actionsSatisfied: (packageId: DependencyId) => boolean + healthCheckSatisfied: ( + packageId: DependencyId, + healthCheckId: HealthCheckId, + ) => boolean + satisfied: () => boolean + + throwIfInstalledNotSatisfied: (packageId: DependencyId) => null + throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null + throwIfRunningNotSatisfied: (packageId: DependencyId) => null + throwIfActionsNotSatisfied: (packageId: DependencyId) => null + throwIfHealthNotSatisfied: ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => null + throwIfNotSatisfied: (packageId?: DependencyId) => null +} +export async function checkDependencies< + DependencyId extends PackageId = PackageId, +>( + effects: Effects, + packageIds?: DependencyId[], +): Promise> { + let [dependencies, results] = await Promise.all([ + effects.getDependencies(), + effects.checkDependencies({ + packageIds, + }), + ]) + if (packageIds) { + dependencies = dependencies.filter((d) => + (packageIds as PackageId[]).includes(d.id), + ) + } + + const find = (packageId: DependencyId) => { + const dependencyRequirement = dependencies.find((d) => d.id === packageId) + const dependencyResult = results.find((d) => d.packageId === packageId) + if (!dependencyRequirement || !dependencyResult) { + throw new Error(`Unknown DependencyId ${packageId}`) + } + return { requirement: dependencyRequirement, result: dependencyResult } + } + + const installedSatisfied = (packageId: DependencyId) => + !!find(packageId).result.installedVersion + const installedVersionSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + return ( + !!dep.result.installedVersion && + ExtendedVersion.parse(dep.result.installedVersion).satisfies( + VersionRange.parse(dep.requirement.versionRange), + ) + ) + } + const runningSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + return dep.requirement.kind !== "running" || dep.result.isRunning + } + const actionsSatisfied = (packageId: DependencyId) => + Object.keys(find(packageId).result.requestedActions).length === 0 + const healthCheckSatisfied = ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => { + const dep = find(packageId) + if ( + healthCheckId && + (dep.requirement.kind !== "running" || + !dep.requirement.healthChecks.includes(healthCheckId)) + ) { + throw new Error(`Unknown HealthCheckId ${healthCheckId}`) + } + const errors = Object.entries(dep.result.healthChecks) + .filter(([id, _]) => (healthCheckId ? id === healthCheckId : true)) + .filter(([_, res]) => res.result !== "success") + return errors.length === 0 + } + const pkgSatisfied = (packageId: DependencyId) => + installedSatisfied(packageId) && + installedVersionSatisfied(packageId) && + runningSatisfied(packageId) && + actionsSatisfied(packageId) && + healthCheckSatisfied(packageId) + const satisfied = (packageId?: DependencyId) => + packageId + ? pkgSatisfied(packageId) + : dependencies.every((d) => pkgSatisfied(d.id as DependencyId)) + + const throwIfInstalledNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.installedVersion) { + throw new Error(`${dep.result.title || packageId} is not installed`) + } + return null + } + const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.installedVersion) { + throw new Error(`${dep.result.title || packageId} is not installed`) + } + if ( + ![dep.result.installedVersion, ...dep.result.satisfies].find((v) => + ExtendedVersion.parse(v).satisfies( + VersionRange.parse(dep.requirement.versionRange), + ), + ) + ) { + throw new Error( + `Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`, + ) + } + return null + } + const throwIfRunningNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (dep.requirement.kind === "running" && !dep.result.isRunning) { + throw new Error(`${dep.result.title || packageId} is not running`) + } + return null + } + const throwIfActionsNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + const reqs = Object.keys(dep.result.requestedActions) + if (reqs.length) { + throw new Error( + `The following action requests have not been fulfilled: ${reqs.join(", ")}`, + ) + } + return null + } + const throwIfHealthNotSatisfied = ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => { + const dep = find(packageId) + if ( + healthCheckId && + (dep.requirement.kind !== "running" || + !dep.requirement.healthChecks.includes(healthCheckId)) + ) { + throw new Error(`Unknown HealthCheckId ${healthCheckId}`) + } + const errors = Object.entries(dep.result.healthChecks) + .filter(([id, _]) => (healthCheckId ? id === healthCheckId : true)) + .filter(([_, res]) => res.result !== "success") + if (errors.length) { + throw new Error( + errors + .map( + ([_, e]) => + `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`, + ) + .join("; "), + ) + } + return null + } + const throwIfPkgNotSatisfied = (packageId: DependencyId) => { + throwIfInstalledNotSatisfied(packageId) + throwIfInstalledVersionNotSatisfied(packageId) + throwIfRunningNotSatisfied(packageId) + throwIfActionsNotSatisfied(packageId) + throwIfHealthNotSatisfied(packageId) + return null + } + const throwIfNotSatisfied = (packageId?: DependencyId) => + packageId + ? throwIfPkgNotSatisfied(packageId) + : (() => { + const err = dependencies.flatMap((d) => { + try { + throwIfPkgNotSatisfied(d.id as DependencyId) + } catch (e) { + if (e instanceof Error) return [e.message] + throw e + } + return [] + }) + if (err.length) { + throw new Error(err.join("; ")) + } + return null + })() + + return { + installedSatisfied, + installedVersionSatisfied, + runningSatisfied, + actionsSatisfied, + healthCheckSatisfied, + satisfied, + throwIfInstalledNotSatisfied, + throwIfInstalledVersionNotSatisfied, + throwIfRunningNotSatisfied, + throwIfActionsNotSatisfied, + throwIfHealthNotSatisfied, + throwIfNotSatisfied, + } +} diff --git a/sdk/base/lib/dependencies/index.ts b/sdk/base/lib/dependencies/index.ts new file mode 100644 index 000000000..09e2b33ad --- /dev/null +++ b/sdk/base/lib/dependencies/index.ts @@ -0,0 +1,6 @@ +// prettier-ignore +export type ReadonlyDeep = + A extends Function ? A : + A extends {} ? { readonly [K in keyof A]: ReadonlyDeep } : A; +export type MaybePromise = Promise | A +export type Message = string diff --git a/sdk/base/lib/dependencies/setupDependencies.ts b/sdk/base/lib/dependencies/setupDependencies.ts new file mode 100644 index 000000000..4583ae749 --- /dev/null +++ b/sdk/base/lib/dependencies/setupDependencies.ts @@ -0,0 +1,64 @@ +import * as T from "../types" +import { once } from "../util" + +export type RequiredDependenciesOf = { + [K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false + ? K + : never +}[keyof Manifest["dependencies"]] +export type OptionalDependenciesOf = Exclude< + keyof Manifest["dependencies"], + RequiredDependenciesOf +> + +type DependencyRequirement = + | { + kind: "running" + healthChecks: Array + versionRange: string + } + | { + kind: "exists" + versionRange: string + } +type Matches = T extends U ? (U extends T ? null : never) : never +const _checkType: Matches< + DependencyRequirement & { id: T.PackageId }, + T.DependencyRequirement +> = null + +export type CurrentDependenciesResult = { + [K in RequiredDependenciesOf]: DependencyRequirement +} & { + [K in OptionalDependenciesOf]?: DependencyRequirement +} + +export function setupDependencies( + fn: (options: { + effects: T.Effects + }) => Promise>, +): (options: { effects: T.Effects }) => Promise { + const cell = { updater: async (_: { effects: T.Effects }) => null } + cell.updater = async (options: { effects: T.Effects }) => { + options.effects = { + ...options.effects, + constRetry: once(() => { + cell.updater(options) + }), + } + const dependencyType = await fn(options) + return await options.effects.setDependencies({ + dependencies: Object.entries(dependencyType) + .map(([k, v]) => [k, v as DependencyRequirement] as const) + .map( + ([id, { versionRange, ...x }]) => + ({ + id, + ...x, + versionRange: versionRange.toString(), + }) as T.DependencyRequirement, + ), + }) + } + return cell.updater +} diff --git a/sdk/base/lib/exver/exver.pegjs b/sdk/base/lib/exver/exver.pegjs new file mode 100644 index 000000000..3045b9224 --- /dev/null +++ b/sdk/base/lib/exver/exver.pegjs @@ -0,0 +1,99 @@ +// #flavor:0.1.2-beta.1:0 +// !( >=1:1 && <= 2:2) + +VersionRange + = first:VersionRangeAtom rest:(_ ((Or / And) _)? VersionRangeAtom)* + +Or = "||" + +And = "&&" + +VersionRangeAtom + = Parens + / Anchor + / Not + / Any + / None + +Parens + = "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } } + +Anchor + = operator:CmpOp? _ version:VersionSpec { return { type: "Anchor", operator, version } } + +VersionSpec + = flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } } + +Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }} + +Any = "*" { return { type: "Any" } } + +None = "!" { return { type: "None" } } + +CmpOp + = ">=" { return ">="; } + / "<=" { return "<="; } + / ">" { return ">"; } + / "<" { return "<"; } + / "=" { return "="; } + / "!=" { return "!="; } + / "^" { return "^"; } + / "~" { return "~"; } + +ExtendedVersion + = flavor:Flavor? upstream:Version ":" downstream:Version { + return { flavor: flavor || null, upstream, downstream } + } + +EmVer + = major:Digit "." minor:Digit "." patch:Digit ("." revision:Digit)? { + return { + flavor: null, + upstream: { + number: [major, minor, patch], + prerelease: [], + }, + downstream: { + number: [revision || 0], + prerelease: [], + }, + } + } + +Flavor + = "#" flavor:Lowercase ":" { return flavor } + +Lowercase + = [a-z]+ { return text() } + +String + = [a-zA-Z]+ { return text(); } + +Version + = number:VersionNumber prerelease: PreRelease? { + return { + number, + prerelease: prerelease || [] + }; + } + +PreRelease + = "-" first:PreReleaseSegment rest:("." PreReleaseSegment)* { + return [first].concat(rest.map(r => r[1])); + } + +PreReleaseSegment + = "."? segment:(Digit / String) { + return segment; + } + +VersionNumber + = first:Digit rest:("." Digit)* { + return [first].concat(rest.map(r => r[1])); + } + +Digit + = [0-9]+ { return parseInt(text(), 10); } + +_ "whitespace" + = [ \t\n\r]* \ No newline at end of file diff --git a/sdk/base/lib/exver/exver.ts b/sdk/base/lib/exver/exver.ts new file mode 100644 index 000000000..be9ea3e0e --- /dev/null +++ b/sdk/base/lib/exver/exver.ts @@ -0,0 +1,2507 @@ +/* eslint-disable */ + + + +const peggyParser: {parse: any, SyntaxError: any, DefaultTracer?: any} = // Generated by Peggy 3.0.2. +// +// https://peggyjs.org/ +// @ts-ignore +(function() { +// @ts-ignore + "use strict"; + +// @ts-ignore +function peg$subclass(child, parent) { +// @ts-ignore + function C() { this.constructor = child; } +// @ts-ignore + C.prototype = parent.prototype; +// @ts-ignore + child.prototype = new C(); +} + +// @ts-ignore +function peg$SyntaxError(message, expected, found, location) { +// @ts-ignore + var self = Error.call(this, message); + // istanbul ignore next Check is a necessary evil to support older environments +// @ts-ignore + if (Object.setPrototypeOf) { +// @ts-ignore + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } +// @ts-ignore + self.expected = expected; +// @ts-ignore + self.found = found; +// @ts-ignore + self.location = location; +// @ts-ignore + self.name = "SyntaxError"; +// @ts-ignore + return self; +} + +// @ts-ignore +peg$subclass(peg$SyntaxError, Error); + +// @ts-ignore +function peg$padEnd(str, targetLength, padString) { +// @ts-ignore + padString = padString || " "; +// @ts-ignore + if (str.length > targetLength) { return str; } +// @ts-ignore + targetLength -= str.length; +// @ts-ignore + padString += padString.repeat(targetLength); +// @ts-ignore + return str + padString.slice(0, targetLength); +} + +// @ts-ignore +peg$SyntaxError.prototype.format = function(sources) { +// @ts-ignore + var str = "Error: " + this.message; +// @ts-ignore + if (this.location) { +// @ts-ignore + var src = null; +// @ts-ignore + var k; +// @ts-ignore + for (k = 0; k < sources.length; k++) { +// @ts-ignore + if (sources[k].source === this.location.source) { +// @ts-ignore + src = sources[k].text.split(/\r\n|\n|\r/g); +// @ts-ignore + break; + } + } +// @ts-ignore + var s = this.location.start; +// @ts-ignore + var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) +// @ts-ignore + ? this.location.source.offset(s) +// @ts-ignore + : s; +// @ts-ignore + var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; +// @ts-ignore + if (src) { +// @ts-ignore + var e = this.location.end; +// @ts-ignore + var filler = peg$padEnd("", offset_s.line.toString().length, ' '); +// @ts-ignore + var line = src[s.line - 1]; +// @ts-ignore + var last = s.line === e.line ? e.column : line.length + 1; +// @ts-ignore + var hatLen = (last - s.column) || 1; +// @ts-ignore + str += "\n --> " + loc + "\n" +// @ts-ignore + + filler + " |\n" +// @ts-ignore + + offset_s.line + " | " + line + "\n" +// @ts-ignore + + filler + " | " + peg$padEnd("", s.column - 1, ' ') +// @ts-ignore + + peg$padEnd("", hatLen, "^"); +// @ts-ignore + } else { +// @ts-ignore + str += "\n at " + loc; + } + } +// @ts-ignore + return str; +}; + +// @ts-ignore +peg$SyntaxError.buildMessage = function(expected, found) { +// @ts-ignore + var DESCRIBE_EXPECTATION_FNS = { +// @ts-ignore + literal: function(expectation) { +// @ts-ignore + return "\"" + literalEscape(expectation.text) + "\""; + }, + +// @ts-ignore + class: function(expectation) { +// @ts-ignore + var escapedParts = expectation.parts.map(function(part) { +// @ts-ignore + return Array.isArray(part) +// @ts-ignore + ? classEscape(part[0]) + "-" + classEscape(part[1]) +// @ts-ignore + : classEscape(part); + }); + +// @ts-ignore + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]"; + }, + +// @ts-ignore + any: function() { +// @ts-ignore + return "any character"; + }, + +// @ts-ignore + end: function() { +// @ts-ignore + return "end of input"; + }, + +// @ts-ignore + other: function(expectation) { +// @ts-ignore + return expectation.description; + } + }; + +// @ts-ignore + function hex(ch) { +// @ts-ignore + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + +// @ts-ignore + function literalEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/"/g, "\\\"") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function classEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/\]/g, "\\]") +// @ts-ignore + .replace(/\^/g, "\\^") +// @ts-ignore + .replace(/-/g, "\\-") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function describeExpectation(expectation) { +// @ts-ignore + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + +// @ts-ignore + function describeExpected(expected) { +// @ts-ignore + var descriptions = expected.map(describeExpectation); +// @ts-ignore + var i, j; + +// @ts-ignore + descriptions.sort(); + +// @ts-ignore + if (descriptions.length > 0) { +// @ts-ignore + for (i = 1, j = 1; i < descriptions.length; i++) { +// @ts-ignore + if (descriptions[i - 1] !== descriptions[i]) { +// @ts-ignore + descriptions[j] = descriptions[i]; +// @ts-ignore + j++; + } + } +// @ts-ignore + descriptions.length = j; + } + +// @ts-ignore + switch (descriptions.length) { +// @ts-ignore + case 1: +// @ts-ignore + return descriptions[0]; + +// @ts-ignore + case 2: +// @ts-ignore + return descriptions[0] + " or " + descriptions[1]; + +// @ts-ignore + default: +// @ts-ignore + return descriptions.slice(0, -1).join(", ") +// @ts-ignore + + ", or " +// @ts-ignore + + descriptions[descriptions.length - 1]; + } + } + +// @ts-ignore + function describeFound(found) { +// @ts-ignore + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + +// @ts-ignore + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +// @ts-ignore +function peg$parse(input, options) { +// @ts-ignore + options = options !== undefined ? options : {}; + +// @ts-ignore + var peg$FAILED = {}; +// @ts-ignore + var peg$source = options.grammarSource; + +// @ts-ignore + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmVer: peg$parseEmVer, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; +// @ts-ignore + var peg$startRuleFunction = peg$parseVersionRange; + +// @ts-ignore + var peg$c0 = "||"; + var peg$c1 = "&&"; + var peg$c2 = "("; + var peg$c3 = ")"; + var peg$c4 = ":"; + var peg$c5 = "!"; + var peg$c6 = "*"; + var peg$c7 = ">="; + var peg$c8 = "<="; + var peg$c9 = ">"; + var peg$c10 = "<"; + var peg$c11 = "="; + var peg$c12 = "!="; + var peg$c13 = "^"; + var peg$c14 = "~"; + var peg$c15 = "."; + var peg$c16 = "#"; + var peg$c17 = "-"; + + var peg$r0 = /^[a-z]/; + var peg$r1 = /^[a-zA-Z]/; + var peg$r2 = /^[0-9]/; + var peg$r3 = /^[ \t\n\r]/; + + var peg$e0 = peg$literalExpectation("||", false); + var peg$e1 = peg$literalExpectation("&&", false); + var peg$e2 = peg$literalExpectation("(", false); + var peg$e3 = peg$literalExpectation(")", false); + var peg$e4 = peg$literalExpectation(":", false); + var peg$e5 = peg$literalExpectation("!", false); + var peg$e6 = peg$literalExpectation("*", false); + var peg$e7 = peg$literalExpectation(">=", false); + var peg$e8 = peg$literalExpectation("<=", false); + var peg$e9 = peg$literalExpectation(">", false); + var peg$e10 = peg$literalExpectation("<", false); + var peg$e11 = peg$literalExpectation("=", false); + var peg$e12 = peg$literalExpectation("!=", false); + var peg$e13 = peg$literalExpectation("^", false); + var peg$e14 = peg$literalExpectation("~", false); + var peg$e15 = peg$literalExpectation(".", false); + var peg$e16 = peg$literalExpectation("#", false); + var peg$e17 = peg$classExpectation([["a", "z"]], false, false); + var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); + var peg$e19 = peg$literalExpectation("-", false); + var peg$e20 = peg$classExpectation([["0", "9"]], false, false); + var peg$e21 = peg$otherExpectation("whitespace"); + var peg$e22 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false); +// @ts-ignore + + var peg$f0 = function(expr) {// @ts-ignore + return { type: "Parens", expr } };// @ts-ignore + + var peg$f1 = function(operator, version) {// @ts-ignore + return { type: "Anchor", operator, version } };// @ts-ignore + + var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore + return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore + + var peg$f3 = function(value) {// @ts-ignore + return { type: "Not", value: value }};// @ts-ignore + + var peg$f4 = function() {// @ts-ignore + return { type: "Any" } };// @ts-ignore + + var peg$f5 = function() {// @ts-ignore + return { type: "None" } };// @ts-ignore + + var peg$f6 = function() {// @ts-ignore + return ">="; };// @ts-ignore + + var peg$f7 = function() {// @ts-ignore + return "<="; };// @ts-ignore + + var peg$f8 = function() {// @ts-ignore + return ">"; };// @ts-ignore + + var peg$f9 = function() {// @ts-ignore + return "<"; };// @ts-ignore + + var peg$f10 = function() {// @ts-ignore + return "="; };// @ts-ignore + + var peg$f11 = function() {// @ts-ignore + return "!="; };// @ts-ignore + + var peg$f12 = function() {// @ts-ignore + return "^"; };// @ts-ignore + + var peg$f13 = function() {// @ts-ignore + return "~"; };// @ts-ignore + + var peg$f14 = function(flavor, upstream, downstream) { +// @ts-ignore + return { flavor: flavor || null, upstream, downstream } + };// @ts-ignore + + var peg$f15 = function(major, minor, patch) { +// @ts-ignore + return { +// @ts-ignore + flavor: null, +// @ts-ignore + upstream: { +// @ts-ignore + number: [major, minor, patch], +// @ts-ignore + prerelease: [], + }, +// @ts-ignore + downstream: { +// @ts-ignore + number: [revision || 0], +// @ts-ignore + prerelease: [], + }, + } + };// @ts-ignore + + var peg$f16 = function(flavor) {// @ts-ignore + return flavor };// @ts-ignore + + var peg$f17 = function() {// @ts-ignore + return text() };// @ts-ignore + + var peg$f18 = function() {// @ts-ignore + return text(); };// @ts-ignore + + var peg$f19 = function(number, prerelease) { +// @ts-ignore + return { +// @ts-ignore + number, +// @ts-ignore + prerelease: prerelease || [] + }; + };// @ts-ignore + + var peg$f20 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f21 = function(segment) { +// @ts-ignore + return segment; + };// @ts-ignore + + var peg$f22 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f23 = function() {// @ts-ignore + return parseInt(text(), 10); }; +// @ts-ignore + var peg$currPos = 0; +// @ts-ignore + var peg$savedPos = 0; +// @ts-ignore + var peg$posDetailsCache = [{ line: 1, column: 1 }]; +// @ts-ignore + var peg$maxFailPos = 0; +// @ts-ignore + var peg$maxFailExpected = []; +// @ts-ignore + var peg$silentFails = 0; + +// @ts-ignore + var peg$result; + +// @ts-ignore + if ("startRule" in options) { +// @ts-ignore + if (!(options.startRule in peg$startRuleFunctions)) { +// @ts-ignore + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + +// @ts-ignore + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + +// @ts-ignore + function text() { +// @ts-ignore + return input.substring(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function offset() { +// @ts-ignore + return peg$savedPos; + } + +// @ts-ignore + function range() { +// @ts-ignore + return { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: peg$savedPos, +// @ts-ignore + end: peg$currPos + }; + } + +// @ts-ignore + function location() { +// @ts-ignore + return peg$computeLocation(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function expected(description, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + [peg$otherExpectation(description)], +// @ts-ignore + input.substring(peg$savedPos, peg$currPos), +// @ts-ignore + location + ); + } + +// @ts-ignore + function error(message, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildSimpleError(message, location); + } + +// @ts-ignore + function peg$literalExpectation(text, ignoreCase) { +// @ts-ignore + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$classExpectation(parts, inverted, ignoreCase) { +// @ts-ignore + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$anyExpectation() { +// @ts-ignore + return { type: "any" }; + } + +// @ts-ignore + function peg$endExpectation() { +// @ts-ignore + return { type: "end" }; + } + +// @ts-ignore + function peg$otherExpectation(description) { +// @ts-ignore + return { type: "other", description: description }; + } + +// @ts-ignore + function peg$computePosDetails(pos) { +// @ts-ignore + var details = peg$posDetailsCache[pos]; +// @ts-ignore + var p; + +// @ts-ignore + if (details) { +// @ts-ignore + return details; +// @ts-ignore + } else { +// @ts-ignore + p = pos - 1; +// @ts-ignore + while (!peg$posDetailsCache[p]) { +// @ts-ignore + p--; + } + +// @ts-ignore + details = peg$posDetailsCache[p]; +// @ts-ignore + details = { +// @ts-ignore + line: details.line, +// @ts-ignore + column: details.column + }; + +// @ts-ignore + while (p < pos) { +// @ts-ignore + if (input.charCodeAt(p) === 10) { +// @ts-ignore + details.line++; +// @ts-ignore + details.column = 1; +// @ts-ignore + } else { +// @ts-ignore + details.column++; + } + +// @ts-ignore + p++; + } + +// @ts-ignore + peg$posDetailsCache[pos] = details; + +// @ts-ignore + return details; + } + } + +// @ts-ignore + function peg$computeLocation(startPos, endPos, offset) { +// @ts-ignore + var startPosDetails = peg$computePosDetails(startPos); +// @ts-ignore + var endPosDetails = peg$computePosDetails(endPos); + +// @ts-ignore + var res = { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: { +// @ts-ignore + offset: startPos, +// @ts-ignore + line: startPosDetails.line, +// @ts-ignore + column: startPosDetails.column + }, +// @ts-ignore + end: { +// @ts-ignore + offset: endPos, +// @ts-ignore + line: endPosDetails.line, +// @ts-ignore + column: endPosDetails.column + } + }; +// @ts-ignore + if (offset && peg$source && (typeof peg$source.offset === "function")) { +// @ts-ignore + res.start = peg$source.offset(res.start); +// @ts-ignore + res.end = peg$source.offset(res.end); + } +// @ts-ignore + return res; + } + +// @ts-ignore + function peg$fail(expected) { +// @ts-ignore + if (peg$currPos < peg$maxFailPos) { return; } + +// @ts-ignore + if (peg$currPos > peg$maxFailPos) { +// @ts-ignore + peg$maxFailPos = peg$currPos; +// @ts-ignore + peg$maxFailExpected = []; + } + +// @ts-ignore + peg$maxFailExpected.push(expected); + } + +// @ts-ignore + function peg$buildSimpleError(message, location) { +// @ts-ignore + return new peg$SyntaxError(message, null, null, location); + } + +// @ts-ignore + function peg$buildStructuredError(expected, found, location) { +// @ts-ignore + return new peg$SyntaxError( +// @ts-ignore + peg$SyntaxError.buildMessage(expected, found), +// @ts-ignore + expected, +// @ts-ignore + found, +// @ts-ignore + location + ); + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRange() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + s1 = [s1, s2]; +// @ts-ignore + s0 = s1; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseOr() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c0) { +// @ts-ignore + s0 = peg$c0; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnd() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c1) { +// @ts-ignore + s0 = peg$c1; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRangeAtom() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseParens(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAnchor(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNot(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAny(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNone(); + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseParens() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s1 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRange(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s5 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f0(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnchor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseCmpOp(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionSpec(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f1(s1, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionSpec() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s4 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseVersion(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + if (s3 === peg$FAILED) { +// @ts-ignore + s3 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f2(s1, s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAny() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 42) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f4(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNone() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f5(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseCmpOp() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c7) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f6(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c8) { +// @ts-ignore + s1 = peg$c8; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f7(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 62) { +// @ts-ignore + s1 = peg$c9; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f8(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 60) { +// @ts-ignore + s1 = peg$c10; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f9(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 61) { +// @ts-ignore + s1 = peg$c11; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f10(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c12) { +// @ts-ignore + s1 = peg$c12; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f11(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 94) { +// @ts-ignore + s1 = peg$c13; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f12(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 126) { +// @ts-ignore + s1 = peg$c14; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f13(); + } +// @ts-ignore + s0 = s1; + } + } + } + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseExtendedVersion() { +// @ts-ignore + var s0, s1, s2, s3, s4; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parseVersion(); +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f14(s1, s2, s4); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmVer() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s2 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$parseDigit(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s7 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s7 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + s8 = peg$parseDigit(); +// @ts-ignore + if (s8 !== peg$FAILED) { +// @ts-ignore + s7 = [s7, s8]; +// @ts-ignore + s6 = s7; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f15(s1, s3, s5); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 35) { +// @ts-ignore + s1 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseLowercase(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f16(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseLowercase() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f17(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseString() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f18(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersion() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionNumber(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreRelease(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f19(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreRelease() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 45) { +// @ts-ignore + s1 = peg$c17; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = []; +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + while (s4 !== peg$FAILED) { +// @ts-ignore + s3.push(s4); +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f20(s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreReleaseSegment() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s1 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseDigit(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = peg$parseString(); + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f21(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionNumber() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f22(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseDigit() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f23(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parse_() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + peg$silentFails++; +// @ts-ignore + s0 = []; +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } +// @ts-ignore + while (s1 !== peg$FAILED) { +// @ts-ignore + s0.push(s1); +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + } +// @ts-ignore + peg$silentFails--; +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e21); } + +// @ts-ignore + return s0; + } + +// @ts-ignore + peg$result = peg$startRuleFunction(); + +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos === input.length) { +// @ts-ignore + return peg$result; +// @ts-ignore + } else { +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos < input.length) { +// @ts-ignore + peg$fail(peg$endExpectation()); + } + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + peg$maxFailExpected, +// @ts-ignore + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, +// @ts-ignore + peg$maxFailPos < input.length +// @ts-ignore + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) +// @ts-ignore + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +// @ts-ignore + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})() + +export interface FilePosition { + offset: number; + line: number; + column: number; +} + +export interface FileRange { + start: FilePosition; + end: FilePosition; + source: string; +} + +export interface LiteralExpectation { + type: "literal"; + text: string; + ignoreCase: boolean; +} + +export interface ClassParts extends Array {} + +export interface ClassExpectation { + type: "class"; + parts: ClassParts; + inverted: boolean; + ignoreCase: boolean; +} + +export interface AnyExpectation { + type: "any"; +} + +export interface EndExpectation { + type: "end"; +} + +export interface OtherExpectation { + type: "other"; + description: string; +} + +export type Expectation = LiteralExpectation | ClassExpectation | AnyExpectation | EndExpectation | OtherExpectation; + +declare class _PeggySyntaxError extends Error { + public static buildMessage(expected: Expectation[], found: string | null): string; + public message: string; + public expected: Expectation[]; + public found: string | null; + public location: FileRange; + public name: string; + constructor(message: string, expected: Expectation[], found: string | null, location: FileRange); + format(sources: { + source?: any; + text: string; + }[]): string; +} + +export interface TraceEvent { + type: string; + rule: string; + result?: any; + location: FileRange; + } + +declare class _DefaultTracer { + private indentLevel: number; + public trace(event: TraceEvent): void; +} + +peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; + +export interface ParseOptions { + filename?: string; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmVer" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + tracer?: any; + [key: string]: any; +} +export type ParseFunction = ( + input: string, + options?: Options + ) => Options extends { startRule: infer StartRule } ? + StartRule extends "VersionRange" ? VersionRange : + StartRule extends "Or" ? Or : + StartRule extends "And" ? And : + StartRule extends "VersionRangeAtom" ? VersionRangeAtom : + StartRule extends "Parens" ? Parens : + StartRule extends "Anchor" ? Anchor : + StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "Not" ? Not : + StartRule extends "Any" ? Any : + StartRule extends "None" ? None : + StartRule extends "CmpOp" ? CmpOp : + StartRule extends "ExtendedVersion" ? ExtendedVersion : + StartRule extends "EmVer" ? EmVer : + StartRule extends "Flavor" ? Flavor : + StartRule extends "Lowercase" ? Lowercase_1 : + StartRule extends "String" ? String_1 : + StartRule extends "Version" ? Version : + StartRule extends "PreRelease" ? PreRelease : + StartRule extends "PreReleaseSegment" ? PreReleaseSegment : + StartRule extends "VersionNumber" ? VersionNumber : + StartRule extends "Digit" ? Digit : + StartRule extends "_" ? _ : VersionRange + : VersionRange; +export const parse: ParseFunction = peggyParser.parse; + +export const PeggySyntaxError = peggyParser.SyntaxError as typeof _PeggySyntaxError; + +export type PeggySyntaxError = _PeggySyntaxError; + +// These types were autogenerated by ts-pegjs +export type VersionRange = [ + VersionRangeAtom, + [_, [Or | And, _] | null, VersionRangeAtom][] +]; +export type Or = "||"; +export type And = "&&"; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None; +export type Parens = { type: "Parens"; expr: VersionRange }; +export type Anchor = { + type: "Anchor"; + operator: CmpOp | null; + version: VersionSpec; +}; +export type VersionSpec = { + flavor: NonNullable | null; + upstream: Version; + downstream: any; +}; +export type Not = { type: "Not"; value: VersionRangeAtom }; +export type Any = { type: "Any" }; +export type None = { type: "None" }; +export type CmpOp = ">=" | "<=" | ">" | "<" | "=" | "!=" | "^" | "~"; +export type ExtendedVersion = { + flavor: NonNullable | null; + upstream: Version; + downstream: Version; +}; +export type EmVer = { + flavor: null; + upstream: { number: [Digit, Digit, Digit]; prerelease: [] }; + downstream: { number: [any]; prerelease: [] }; +}; +export type Flavor = Lowercase_1; +export type Lowercase_1 = string; +export type String_1 = string; +export type Version = { + number: VersionNumber; + prerelease: never[] | NonNullable; +}; +export type PreRelease = PreReleaseSegment[]; +export type PreReleaseSegment = Digit | String_1; +export type VersionNumber = Digit[]; +export type Digit = number; +export type _ = string[]; diff --git a/sdk/base/lib/exver/index.ts b/sdk/base/lib/exver/index.ts new file mode 100644 index 000000000..331271c1a --- /dev/null +++ b/sdk/base/lib/exver/index.ts @@ -0,0 +1,454 @@ +import * as P from "./exver" + +// prettier-ignore +export type ValidateVersion = +T extends `-${infer A}` ? never : +T extends `${infer A}-${string}` ? ValidateVersion : + T extends `${bigint}` ? unknown : + T extends `${bigint}.${infer A}` ? ValidateVersion : + never + +// prettier-ignore +export type ValidateExVer = + T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + never + +// prettier-ignore +export type ValidateExVers = + T extends [] ? unknown[] : + T extends [infer A, ...infer B] ? ValidateExVer & ValidateExVers : + never[] + +type Anchor = { + type: "Anchor" + operator: P.CmpOp + version: ExtendedVersion +} + +type And = { + type: "And" + left: VersionRange + right: VersionRange +} + +type Or = { + type: "Or" + left: VersionRange + right: VersionRange +} + +type Not = { + type: "Not" + value: VersionRange +} + +export class VersionRange { + private constructor(public atom: Anchor | And | Or | Not | P.Any | P.None) {} + + toString(): string { + switch (this.atom.type) { + case "Anchor": + return `${this.atom.operator}${this.atom.version}` + case "And": + return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})` + case "Or": + return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})` + case "Not": + return `!(${this.atom.value.toString()})` + case "Any": + return "*" + case "None": + return "!" + } + } + + private static parseAtom(atom: P.VersionRangeAtom): VersionRange { + switch (atom.type) { + case "Not": + return new VersionRange({ + type: "Not", + value: VersionRange.parseAtom(atom.value), + }) + case "Parens": + return VersionRange.parseRange(atom.expr) + case "Anchor": + return new VersionRange({ + type: "Anchor", + operator: atom.operator || "^", + version: new ExtendedVersion( + atom.version.flavor, + new Version( + atom.version.upstream.number, + atom.version.upstream.prerelease, + ), + new Version( + atom.version.downstream.number, + atom.version.downstream.prerelease, + ), + ), + }) + default: + return new VersionRange(atom) + } + } + + private static parseRange(range: P.VersionRange): VersionRange { + let result = VersionRange.parseAtom(range[0]) + for (const next of range[1]) { + switch (next[1]?.[0]) { + case "||": + result = new VersionRange({ + type: "Or", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + case "&&": + default: + result = new VersionRange({ + type: "And", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + } + } + return result + } + + static parse(range: string): VersionRange { + return VersionRange.parseRange( + P.parse(range, { startRule: "VersionRange" }), + ) + } + + and(right: VersionRange) { + return new VersionRange({ type: "And", left: this, right }) + } + + or(right: VersionRange) { + return new VersionRange({ type: "Or", left: this, right }) + } + + not() { + return new VersionRange({ type: "Not", value: this }) + } + + static anchor(operator: P.CmpOp, version: ExtendedVersion) { + return new VersionRange({ type: "Anchor", operator, version }) + } + + static any() { + return new VersionRange({ type: "Any" }) + } + + static none() { + return new VersionRange({ type: "None" }) + } + + satisfiedBy(version: Version | ExtendedVersion) { + return version.satisfies(this) + } +} + +export class Version { + constructor( + public number: number[], + public prerelease: (string | number)[], + ) {} + + toString(): string { + return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}` + } + + compare(other: Version): "greater" | "equal" | "less" { + const numLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < numLen; i++) { + if ((this.number[i] || 0) > (other.number[i] || 0)) { + return "greater" + } else if ((this.number[i] || 0) < (other.number[i] || 0)) { + return "less" + } + } + + if (this.prerelease.length === 0 && other.prerelease.length !== 0) { + return "greater" + } else if (this.prerelease.length !== 0 && other.prerelease.length === 0) { + return "less" + } + + const prereleaseLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < prereleaseLen; i++) { + if (typeof this.prerelease[i] === typeof other.prerelease[i]) { + if (this.prerelease[i] > other.prerelease[i]) { + return "greater" + } else if (this.prerelease[i] < other.prerelease[i]) { + return "less" + } + } else { + switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) { + case "number:string": + return "less" + case "string:number": + return "greater" + case "number:undefined": + case "string:undefined": + return "greater" + case "undefined:number": + case "undefined:string": + return "less" + } + } + } + + return "equal" + } + + static parse(version: string): Version { + const parsed = P.parse(version, { startRule: "Version" }) + return new Version(parsed.number, parsed.prerelease) + } + + satisfies(versionRange: VersionRange): boolean { + return new ExtendedVersion(null, this, new Version([0], [])).satisfies( + versionRange, + ) + } +} + +// #flavor:0.1.2-beta.1:0 +export class ExtendedVersion { + constructor( + public flavor: string | null, + public upstream: Version, + public downstream: Version, + ) {} + + toString(): string { + return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}` + } + + compare(other: ExtendedVersion): "greater" | "equal" | "less" | null { + if (this.flavor !== other.flavor) { + return null + } + const upstreamCmp = this.upstream.compare(other.upstream) + if (upstreamCmp !== "equal") { + return upstreamCmp + } + return this.downstream.compare(other.downstream) + } + + compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" { + if ((this.flavor || "") > (other.flavor || "")) { + return "greater" + } else if ((this.flavor || "") > (other.flavor || "")) { + return "less" + } else { + return this.compare(other)! + } + } + + compareForSort(other: ExtendedVersion): 1 | 0 | -1 { + switch (this.compareLexicographic(other)) { + case "greater": + return 1 + case "equal": + return 0 + case "less": + return -1 + } + } + + greaterThan(other: ExtendedVersion): boolean { + return this.compare(other) === "greater" + } + + greaterThanOrEqual(other: ExtendedVersion): boolean { + return ["greater", "equal"].includes(this.compare(other) as string) + } + + equals(other: ExtendedVersion): boolean { + return this.compare(other) === "equal" + } + + lessThan(other: ExtendedVersion): boolean { + return this.compare(other) === "less" + } + + lessThanOrEqual(other: ExtendedVersion): boolean { + return ["less", "equal"].includes(this.compare(other) as string) + } + + static parse(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + static parseEmver(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "EmVer" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + /** + * Returns an ExtendedVersion with the Upstream major version version incremented by 1 + * and sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last upstream digit will be incremented. + */ + incrementMajor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > majorIdx) { + return 0 + } else if (idx === majorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } + + /** + * Returns an ExtendedVersion with the Upstream minor version version incremented by 1 + * also sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last digit will be incremented. + */ + incrementMinor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1 + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > minorIdx) { + return 0 + } else if (idx === minorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } + + /** + * Returns a boolean indicating whether a given version satisfies the VersionRange + * !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0 + */ + satisfies(versionRange: VersionRange): boolean { + switch (versionRange.atom.type) { + case "Anchor": + const otherVersion = versionRange.atom.version + switch (versionRange.atom.operator) { + case "=": + return this.equals(otherVersion) + case ">": + return this.greaterThan(otherVersion) + case "<": + return this.lessThan(otherVersion) + case ">=": + return this.greaterThanOrEqual(otherVersion) + case "<=": + return this.lessThanOrEqual(otherVersion) + case "!=": + return !this.equals(otherVersion) + case "^": + const nextMajor = versionRange.atom.version.incrementMajor() + if ( + this.greaterThanOrEqual(otherVersion) && + this.lessThan(nextMajor) + ) { + return true + } else { + return false + } + case "~": + const nextMinor = versionRange.atom.version.incrementMinor() + if ( + this.greaterThanOrEqual(otherVersion) && + this.lessThan(nextMinor) + ) { + return true + } else { + return false + } + } + case "And": + return ( + this.satisfies(versionRange.atom.left) && + this.satisfies(versionRange.atom.right) + ) + case "Or": + return ( + this.satisfies(versionRange.atom.left) || + this.satisfies(versionRange.atom.right) + ) + case "Not": + return !this.satisfies(versionRange.atom.value) + case "Any": + return true + case "None": + return false + } + } +} + +export const testTypeExVer = (t: T & ValidateExVer) => t + +export const testTypeVersion = (t: T & ValidateVersion) => + t +function tests() { + testTypeVersion("1.2.3") + testTypeVersion("1") + testTypeVersion("12.34.56") + testTypeVersion("1.2-3") + testTypeVersion("1-3") + testTypeVersion("1-alpha") + // @ts-expect-error + testTypeVersion("-3") + // @ts-expect-error + testTypeVersion("1.2.3:1") + // @ts-expect-error + testTypeVersion("#cat:1:1") + + testTypeExVer("1.2.3:1.2.3") + testTypeExVer("1.2.3.4.5.6.7.8.9.0:1") + testTypeExVer("100:1") + testTypeExVer("#cat:1:1") + testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1") + testTypeExVer("1-0:1") + testTypeExVer("1-0:1") + // @ts-expect-error + testTypeExVer("1.2-3") + // @ts-expect-error + testTypeExVer("1-3") + // @ts-expect-error + testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string) + // @ts-expect-error + testTypeExVer("1.-2:1") + // @ts-expect-error + testTypeExVer("1..2.3:3") +} diff --git a/sdk/base/lib/index.ts b/sdk/base/lib/index.ts new file mode 100644 index 000000000..0aa8e4758 --- /dev/null +++ b/sdk/base/lib/index.ts @@ -0,0 +1,12 @@ +export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" + +export * as inputSpec from "./actions/input" +export * as ISB from "./actions/input/builder" +export * as IST from "./actions/input/inputSpecTypes" +export * as types from "./types" +export * as T from "./types" +export * as yaml from "yaml" +export * as matches from "ts-matches" + +export * as utils from "./util" diff --git a/sdk/base/lib/interfaces/AddressReceipt.ts b/sdk/base/lib/interfaces/AddressReceipt.ts new file mode 100644 index 000000000..d57d85685 --- /dev/null +++ b/sdk/base/lib/interfaces/AddressReceipt.ts @@ -0,0 +1,4 @@ +declare const AddressProof: unique symbol +export type AddressReceipt = { + [AddressProof]: never +} diff --git a/sdk/base/lib/interfaces/Host.ts b/sdk/base/lib/interfaces/Host.ts new file mode 100644 index 000000000..53419357f --- /dev/null +++ b/sdk/base/lib/interfaces/Host.ts @@ -0,0 +1,189 @@ +import { object, string } from "ts-matches" +import { Effects } from "../Effects" +import { Origin } from "./Origin" +import { AddSslOptions, BindParams } from "../osBindings" +import { Security } from "../osBindings" +import { BindOptions } from "../osBindings" +import { AlpnInfo } from "../osBindings" + +export { AddSslOptions, Security, BindOptions } + +export const knownProtocols = { + http: { + secure: null, + defaultPort: 80, + withSsl: "https", + alpn: { specified: ["http/1.1"] } as AlpnInfo, + }, + https: { + secure: { ssl: true }, + defaultPort: 443, + }, + ws: { + secure: null, + defaultPort: 80, + withSsl: "wss", + alpn: { specified: ["http/1.1"] } as AlpnInfo, + }, + wss: { + secure: { ssl: true }, + defaultPort: 443, + }, + ssh: { + secure: { ssl: false }, + defaultPort: 22, + }, + bitcoin: { + secure: { ssl: false }, + defaultPort: 8333, + }, + lightning: { + secure: { ssl: true }, + defaultPort: 9735, + }, + grpc: { + secure: { ssl: true }, + defaultPort: 50051, + }, + dns: { + secure: { ssl: false }, + defaultPort: 53, + }, +} as const + +export type Scheme = string | null + +type KnownProtocols = typeof knownProtocols +type ProtocolsWithSslVariants = { + [K in keyof KnownProtocols]: KnownProtocols[K] extends { + withSsl: string + } + ? K + : never +}[keyof KnownProtocols] +type NotProtocolsWithSslVariants = Exclude< + keyof KnownProtocols, + ProtocolsWithSslVariants +> + +type BindOptionsByKnownProtocol = + | { + protocol: ProtocolsWithSslVariants + preferredExternalPort?: number + addSsl?: Partial + } + | { + protocol: NotProtocolsWithSslVariants + preferredExternalPort?: number + addSsl?: AddSslOptions + } +export type BindOptionsByProtocol = + | BindOptionsByKnownProtocol + | (BindOptions & { protocol: null }) + +const hasStringProtocol = object({ + protocol: string, +}).test + +export class MultiHost { + constructor( + readonly options: { + effects: Effects + id: string + }, + ) {} + + /** + * @description Use this function to bind the host to an internal port and configured options for protocol, security, and external port. + * + * @param internalPort - The internal port to be bound. + * @param options - The protocol options for this binding. + * @returns A multi-origin that is capable of exporting one or more service interfaces. + * @example + * In this example, we bind a previously created multi-host to port 80, then select the http protocol and request an external port of 8332. + * + * ``` + const uiMultiOrigin = await uiMulti.bindPort(80, { + protocol: 'http', + preferredExternalPort: 8332, + }) + * ``` + */ + async bindPort( + internalPort: number, + options: BindOptionsByProtocol, + ): Promise { + if (hasStringProtocol(options)) { + return await this.bindPortForKnown(options, internalPort) + } else { + return await this.bindPortForUnknown(internalPort, options) + } + } + + private async bindPortForUnknown( + internalPort: number, + options: { + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: { ssl: boolean } | null + }, + ) { + const binderOptions = { + id: this.options.id, + internalPort, + ...options, + } + await this.options.effects.bind(binderOptions) + + return new Origin(this, internalPort, null, null) + } + + private async bindPortForKnown( + options: BindOptionsByKnownProtocol, + internalPort: number, + ) { + const protoInfo = knownProtocols[options.protocol] + const preferredExternalPort = + options.preferredExternalPort || + knownProtocols[options.protocol].defaultPort + const sslProto = this.getSslProto(options, protoInfo) + const addSsl = + sslProto && "alpn" in protoInfo + ? { + // addXForwardedHeaders: null, + preferredExternalPort: knownProtocols[sslProto].defaultPort, + scheme: sslProto, + alpn: protoInfo.alpn, + ...("addSsl" in options ? options.addSsl : null), + } + : null + + const secure: Security | null = !protoInfo.secure ? null : { ssl: false } + + await this.options.effects.bind({ + id: this.options.id, + internalPort, + preferredExternalPort, + addSsl, + secure, + }) + + return new Origin(this, internalPort, options.protocol, sslProto) + } + + private getSslProto( + options: BindOptionsByKnownProtocol, + protoInfo: KnownProtocols[keyof KnownProtocols], + ) { + if (inObject("noAddSsl", options) && options.noAddSsl) return null + if ("withSsl" in protoInfo && protoInfo.withSsl) return protoInfo.withSsl + return null + } +} + +function inObject( + key: Key, + obj: any, +): obj is { [K in Key]: unknown } { + return key in obj +} diff --git a/sdk/base/lib/interfaces/Origin.ts b/sdk/base/lib/interfaces/Origin.ts new file mode 100644 index 000000000..dd688b3c9 --- /dev/null +++ b/sdk/base/lib/interfaces/Origin.ts @@ -0,0 +1,86 @@ +import { AddressInfo } from "../types" +import { AddressReceipt } from "./AddressReceipt" +import { MultiHost, Scheme } from "./Host" +import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder" + +export class Origin { + constructor( + readonly host: MultiHost, + readonly internalPort: number, + readonly scheme: string | null, + readonly sslScheme: string | null, + ) {} + + build({ username, path, search, schemeOverride }: BuildOptions): AddressInfo { + const qpEntries = Object.entries(search) + .map( + ([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`, + ) + .join("&") + + const qp = qpEntries.length ? `?${qpEntries}` : "" + + return { + hostId: this.host.options.id, + internalPort: this.internalPort, + scheme: schemeOverride ? schemeOverride.noSsl : this.scheme, + sslScheme: schemeOverride ? schemeOverride.ssl : this.sslScheme, + suffix: `${path}${qp}`, + username, + } + } + + /** + * @description A function to register a group of origins ( :// : ) with StartOS + * + * The returned addressReceipt serves as proof that the addresses were registered + * + * @param addressInfo + * @returns + */ + async export( + serviceInterfaces: ServiceInterfaceBuilder[], + ): Promise { + const addressesInfo = [] + for (let serviceInterface of serviceInterfaces) { + const { + name, + description, + id, + type, + username, + path, + search, + schemeOverride, + masked, + } = serviceInterface.options + + const addressInfo = this.build({ + username, + path, + search, + schemeOverride, + }) + + await serviceInterface.options.effects.exportServiceInterface({ + id, + name, + description, + addressInfo, + type, + masked, + }) + + addressesInfo.push(addressInfo) + } + + return addressesInfo as AddressInfo[] & AddressReceipt + } +} + +type BuildOptions = { + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + username: string | null + path: string + search: Record +} diff --git a/sdk/base/lib/interfaces/ServiceInterfaceBuilder.ts b/sdk/base/lib/interfaces/ServiceInterfaceBuilder.ts new file mode 100644 index 000000000..036180ad3 --- /dev/null +++ b/sdk/base/lib/interfaces/ServiceInterfaceBuilder.ts @@ -0,0 +1,31 @@ +import { ServiceInterfaceType } from "../types" +import { Effects } from "../Effects" +import { Scheme } from "./Host" + +/** + * A helper class for creating a Network Interface + * + * Network Interfaces are collections of web addresses that expose the same API or other resource, + * display to the user with under a common name and description. + * + * All URIs on an interface inherit the same ui: bool, basic auth credentials, path, and search (query) params + * + * @param options + * @returns + */ +export class ServiceInterfaceBuilder { + constructor( + readonly options: { + effects: Effects + name: string + id: string + description: string + type: ServiceInterfaceType + username: string | null + path: string + search: Record + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + masked: boolean + }, + ) {} +} diff --git a/sdk/base/lib/interfaces/interfaceReceipt.ts b/sdk/base/lib/interfaces/interfaceReceipt.ts new file mode 100644 index 000000000..24873e67e --- /dev/null +++ b/sdk/base/lib/interfaces/interfaceReceipt.ts @@ -0,0 +1,4 @@ +declare const InterfaceProof: unique symbol +export type InterfaceReceipt = { + [InterfaceProof]: never +} diff --git a/sdk/base/lib/interfaces/setupInterfaces.ts b/sdk/base/lib/interfaces/setupInterfaces.ts new file mode 100644 index 000000000..ba284bcb3 --- /dev/null +++ b/sdk/base/lib/interfaces/setupInterfaces.ts @@ -0,0 +1,57 @@ +import * as T from "../types" +import { once } from "../util" +import { AddressReceipt } from "./AddressReceipt" + +declare const UpdateServiceInterfacesProof: unique symbol +export type UpdateServiceInterfacesReceipt = { + [UpdateServiceInterfacesProof]: never +} + +export type ServiceInterfacesReceipt = Array +export type SetServiceInterfaces = + (opts: { effects: T.Effects }) => Promise +export type UpdateServiceInterfaces = + (opts: { + effects: T.Effects + }) => Promise +export type SetupServiceInterfaces = ( + fn: SetServiceInterfaces, +) => UpdateServiceInterfaces +export const NO_INTERFACE_CHANGES = {} as UpdateServiceInterfacesReceipt +export const setupServiceInterfaces: SetupServiceInterfaces = < + Output extends ServiceInterfacesReceipt, +>( + fn: SetServiceInterfaces, +) => { + const cell = { + updater: (async (options: { effects: T.Effects }) => + [] as any as Output) as UpdateServiceInterfaces, + } + cell.updater = (async (options: { effects: T.Effects }) => { + options.effects = { + ...options.effects, + constRetry: once(() => { + cell.updater(options) + }), + } + const bindings: T.BindId[] = [] + const interfaces: T.ServiceInterfaceId[] = [] + const res = await fn({ + effects: { + ...options.effects, + bind: (params: T.BindParams) => { + bindings.push({ id: params.id, internalPort: params.internalPort }) + return options.effects.bind(params) + }, + exportServiceInterface: (params: T.ExportServiceInterfaceParams) => { + interfaces.push(params.id) + return options.effects.exportServiceInterface(params) + }, + }, + }) + await options.effects.clearBindings({ except: bindings }) + await options.effects.clearServiceInterfaces({ except: interfaces }) + return res + }) as UpdateServiceInterfaces + return cell.updater +} diff --git a/sdk/base/lib/osBindings/AcceptSigners.ts b/sdk/base/lib/osBindings/AcceptSigners.ts new file mode 100644 index 000000000..1ef417623 --- /dev/null +++ b/sdk/base/lib/osBindings/AcceptSigners.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnyVerifyingKey } from "./AnyVerifyingKey" + +export type AcceptSigners = + | { signer: AnyVerifyingKey } + | { any: Array } + | { all: Array } diff --git a/sdk/base/lib/osBindings/AcmeProvider.ts b/sdk/base/lib/osBindings/AcmeProvider.ts new file mode 100644 index 000000000..0ad3f0052 --- /dev/null +++ b/sdk/base/lib/osBindings/AcmeProvider.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AcmeProvider = string diff --git a/sdk/base/lib/osBindings/AcmeSettings.ts b/sdk/base/lib/osBindings/AcmeSettings.ts new file mode 100644 index 000000000..44e70d9df --- /dev/null +++ b/sdk/base/lib/osBindings/AcmeSettings.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AcmeSettings = { contact: Array } diff --git a/sdk/base/lib/osBindings/ActionId.ts b/sdk/base/lib/osBindings/ActionId.ts new file mode 100644 index 000000000..ebfaba49e --- /dev/null +++ b/sdk/base/lib/osBindings/ActionId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionId = string diff --git a/sdk/base/lib/osBindings/ActionInput.ts b/sdk/base/lib/osBindings/ActionInput.ts new file mode 100644 index 000000000..a19a5f1a4 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionInput.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionInput = { + spec: Record + value: Record | null +} diff --git a/sdk/base/lib/osBindings/ActionMetadata.ts b/sdk/base/lib/osBindings/ActionMetadata.ts new file mode 100644 index 000000000..01809ab57 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionMetadata.ts @@ -0,0 +1,37 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionVisibility } from "./ActionVisibility" +import type { AllowedStatuses } from "./AllowedStatuses" + +export type ActionMetadata = { + /** + * A human-readable name + */ + name: string + /** + * A detailed description of what the action will do + */ + description: string + /** + * Presents as an alert prior to executing the action. Should be used sparingly but important if the action could have harmful, unintended consequences + */ + warning: string | null + /** + * One of: "enabled", "hidden", or { disabled: "" } + * - "enabled" - the action is available be run + * - "hidden" - the action cannot be seen or run + * - { disabled: "example explanation" } means the action is visible but cannot be run. Replace "example explanation" with a reason why the action is disable to prevent user confusion. + */ + visibility: ActionVisibility + /** + * One of: "only-stopped", "only-running", "all" + * - "only-stopped" - the action can only be run when the service is stopped + * - "only-running" - the action can only be run when the service is running + * - "any" - the action can only be run regardless of the service's status + */ + allowedStatuses: AllowedStatuses + hasInput: boolean + /** + * If provided, this action will be nested under a header of this value, along with other actions of the same group + */ + group: string | null +} diff --git a/sdk/base/lib/osBindings/ActionRequest.ts b/sdk/base/lib/osBindings/ActionRequest.ts new file mode 100644 index 000000000..552f37bc6 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionRequest.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { ActionRequestInput } from "./ActionRequestInput" +import type { ActionRequestTrigger } from "./ActionRequestTrigger" +import type { ActionSeverity } from "./ActionSeverity" +import type { PackageId } from "./PackageId" + +export type ActionRequest = { + packageId: PackageId + actionId: ActionId + severity: ActionSeverity + reason?: string + when?: ActionRequestTrigger + input?: ActionRequestInput +} diff --git a/sdk/base/lib/osBindings/ActionRequestCondition.ts b/sdk/base/lib/osBindings/ActionRequestCondition.ts new file mode 100644 index 000000000..0f06caf3c --- /dev/null +++ b/sdk/base/lib/osBindings/ActionRequestCondition.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionRequestCondition = "input-not-matches" diff --git a/sdk/base/lib/osBindings/ActionRequestEntry.ts b/sdk/base/lib/osBindings/ActionRequestEntry.ts new file mode 100644 index 000000000..0e716abe4 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionRequestEntry.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionRequest } from "./ActionRequest" + +export type ActionRequestEntry = { request: ActionRequest; active: boolean } diff --git a/sdk/base/lib/osBindings/ActionRequestInput.ts b/sdk/base/lib/osBindings/ActionRequestInput.ts new file mode 100644 index 000000000..a1cde7789 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionRequestInput.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionRequestInput = { + kind: "partial" + value: Record +} diff --git a/sdk/base/lib/osBindings/ActionRequestTrigger.ts b/sdk/base/lib/osBindings/ActionRequestTrigger.ts new file mode 100644 index 000000000..ebd0963e5 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionRequestTrigger.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionRequestCondition } from "./ActionRequestCondition" + +export type ActionRequestTrigger = { + once: boolean + condition: ActionRequestCondition +} diff --git a/sdk/base/lib/osBindings/ActionResult.ts b/sdk/base/lib/osBindings/ActionResult.ts new file mode 100644 index 000000000..7422dcde3 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResult.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionResultV0 } from "./ActionResultV0" +import type { ActionResultV1 } from "./ActionResultV1" + +export type ActionResult = + | ({ version: "0" } & ActionResultV0) + | ({ version: "1" } & ActionResultV1) diff --git a/sdk/base/lib/osBindings/ActionResultMember.ts b/sdk/base/lib/osBindings/ActionResultMember.ts new file mode 100644 index 000000000..c27a6a3a9 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultMember.ts @@ -0,0 +1,39 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionResultMember = { + /** + * A human-readable name or title of the value, such as "Last Active" or "Login Password" + */ + name: string + /** + * (optional) A description of the value, such as an explaining why it exists or how to use it + */ + description: string | null +} & ( + | { + type: "single" + /** + * The actual string value to display + */ + value: string + /** + * Whether or not to include a copy to clipboard icon to copy the value + */ + copyable: boolean + /** + * Whether or not to also display the value as a QR code + */ + qr: boolean + /** + * Whether or not to mask the value using ●●●●●●●, which is useful for password or other sensitive information + */ + masked: boolean + } + | { + type: "group" + /** + * An new group of nested values, experienced by the user as an accordion dropdown + */ + value: Array + } +) diff --git a/sdk/base/lib/osBindings/ActionResultV0.ts b/sdk/base/lib/osBindings/ActionResultV0.ts new file mode 100644 index 000000000..7c6b43d45 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultV0.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionResultV0 = { + message: string + value: string | null + copyable: boolean + qr: boolean +} diff --git a/sdk/base/lib/osBindings/ActionResultV1.ts b/sdk/base/lib/osBindings/ActionResultV1.ts new file mode 100644 index 000000000..ee06ebab9 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultV1.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionResultValue } from "./ActionResultValue" + +export type ActionResultV1 = { + /** + * Primary text to display as the header of the response modal. e.g. "Success!", "Name Updated", or "Service Information", whatever makes sense + */ + title: string + /** + * (optional) A general message for the user, just under the title + */ + message: string | null + /** + * (optional) Structured data to present inside the modal + */ + result: ActionResultValue | null +} diff --git a/sdk/base/lib/osBindings/ActionResultValue.ts b/sdk/base/lib/osBindings/ActionResultValue.ts new file mode 100644 index 000000000..3ffabef8b --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultValue.ts @@ -0,0 +1,30 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionResultMember } from "./ActionResultMember" + +export type ActionResultValue = + | { + type: "single" + /** + * The actual string value to display + */ + value: string + /** + * Whether or not to include a copy to clipboard icon to copy the value + */ + copyable: boolean + /** + * Whether or not to also display the value as a QR code + */ + qr: boolean + /** + * Whether or not to mask the value using ●●●●●●●, which is useful for password or other sensitive information + */ + masked: boolean + } + | { + type: "group" + /** + * An new group of nested values, experienced by the user as an accordion dropdown + */ + value: Array + } diff --git a/sdk/base/lib/osBindings/ActionSeverity.ts b/sdk/base/lib/osBindings/ActionSeverity.ts new file mode 100644 index 000000000..ad339f951 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionSeverity.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionSeverity = "critical" | "important" diff --git a/sdk/base/lib/osBindings/ActionVisibility.ts b/sdk/base/lib/osBindings/ActionVisibility.ts new file mode 100644 index 000000000..ab1e6e1b9 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionVisibility.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionVisibility = "hidden" | { disabled: string } | "enabled" diff --git a/sdk/base/lib/osBindings/AddAdminParams.ts b/sdk/base/lib/osBindings/AddAdminParams.ts new file mode 100644 index 000000000..9da08b54b --- /dev/null +++ b/sdk/base/lib/osBindings/AddAdminParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" + +export type AddAdminParams = { signer: Guid } diff --git a/sdk/base/lib/osBindings/AddAssetParams.ts b/sdk/base/lib/osBindings/AddAssetParams.ts new file mode 100644 index 000000000..7522a1cb4 --- /dev/null +++ b/sdk/base/lib/osBindings/AddAssetParams.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnySignature } from "./AnySignature" +import type { Blake3Commitment } from "./Blake3Commitment" + +export type AddAssetParams = { + version: string + platform: string + url: string + signature: AnySignature + commitment: Blake3Commitment +} diff --git a/sdk/base/lib/osBindings/AddCategoryParams.ts b/sdk/base/lib/osBindings/AddCategoryParams.ts new file mode 100644 index 000000000..799f2d4d2 --- /dev/null +++ b/sdk/base/lib/osBindings/AddCategoryParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddCategoryParams = { + id: string + name: string + short: string + long: string +} diff --git a/sdk/base/lib/osBindings/AddPackageParams.ts b/sdk/base/lib/osBindings/AddPackageParams.ts new file mode 100644 index 000000000..4395b9b8a --- /dev/null +++ b/sdk/base/lib/osBindings/AddPackageParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnySignature } from "./AnySignature" +import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" + +export type AddPackageParams = { + url: string + commitment: MerkleArchiveCommitment + signature: AnySignature +} diff --git a/sdk/base/lib/osBindings/AddSslOptions.ts b/sdk/base/lib/osBindings/AddSslOptions.ts new file mode 100644 index 000000000..35071aff3 --- /dev/null +++ b/sdk/base/lib/osBindings/AddSslOptions.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AlpnInfo } from "./AlpnInfo" + +export type AddSslOptions = { + preferredExternalPort: number + alpn: AlpnInfo | null +} diff --git a/sdk/base/lib/osBindings/AddVersionParams.ts b/sdk/base/lib/osBindings/AddVersionParams.ts new file mode 100644 index 000000000..9fc281a6f --- /dev/null +++ b/sdk/base/lib/osBindings/AddVersionParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddVersionParams = { + version: string + headline: string + releaseNotes: string + sourceVersion: string +} diff --git a/sdk/base/lib/osBindings/AddressInfo.ts b/sdk/base/lib/osBindings/AddressInfo.ts new file mode 100644 index 000000000..c7a1c1af1 --- /dev/null +++ b/sdk/base/lib/osBindings/AddressInfo.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostId } from "./HostId" + +export type AddressInfo = { + username: string | null + hostId: HostId + internalPort: number + scheme: string | null + sslScheme: string | null + suffix: string +} diff --git a/sdk/base/lib/osBindings/Alerts.ts b/sdk/base/lib/osBindings/Alerts.ts new file mode 100644 index 000000000..819d1c407 --- /dev/null +++ b/sdk/base/lib/osBindings/Alerts.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Alerts = { + install: string | null + uninstall: string | null + restore: string | null + start: string | null + stop: string | null +} diff --git a/sdk/base/lib/osBindings/Algorithm.ts b/sdk/base/lib/osBindings/Algorithm.ts new file mode 100644 index 000000000..94f2040e1 --- /dev/null +++ b/sdk/base/lib/osBindings/Algorithm.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Algorithm = "ecdsa" | "ed25519" diff --git a/sdk/base/lib/osBindings/AllPackageData.ts b/sdk/base/lib/osBindings/AllPackageData.ts new file mode 100644 index 000000000..b51b41bf5 --- /dev/null +++ b/sdk/base/lib/osBindings/AllPackageData.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageDataEntry } from "./PackageDataEntry" +import type { PackageId } from "./PackageId" + +export type AllPackageData = { [key: PackageId]: PackageDataEntry } diff --git a/sdk/base/lib/osBindings/AllowedStatuses.ts b/sdk/base/lib/osBindings/AllowedStatuses.ts new file mode 100644 index 000000000..ed5851495 --- /dev/null +++ b/sdk/base/lib/osBindings/AllowedStatuses.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AllowedStatuses = "only-running" | "only-stopped" | "any" diff --git a/sdk/base/lib/osBindings/AlpnInfo.ts b/sdk/base/lib/osBindings/AlpnInfo.ts new file mode 100644 index 000000000..2dacb3d3f --- /dev/null +++ b/sdk/base/lib/osBindings/AlpnInfo.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MaybeUtf8String } from "./MaybeUtf8String" + +export type AlpnInfo = "reflect" | { specified: Array } diff --git a/sdk/base/lib/osBindings/AnySignature.ts b/sdk/base/lib/osBindings/AnySignature.ts new file mode 100644 index 000000000..32b68b911 --- /dev/null +++ b/sdk/base/lib/osBindings/AnySignature.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AnySignature = string diff --git a/sdk/base/lib/osBindings/AnySigningKey.ts b/sdk/base/lib/osBindings/AnySigningKey.ts new file mode 100644 index 000000000..4933251e8 --- /dev/null +++ b/sdk/base/lib/osBindings/AnySigningKey.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AnySigningKey = string diff --git a/sdk/base/lib/osBindings/AnyVerifyingKey.ts b/sdk/base/lib/osBindings/AnyVerifyingKey.ts new file mode 100644 index 000000000..58a45aaa7 --- /dev/null +++ b/sdk/base/lib/osBindings/AnyVerifyingKey.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AnyVerifyingKey = string diff --git a/sdk/base/lib/osBindings/ApiState.ts b/sdk/base/lib/osBindings/ApiState.ts new file mode 100644 index 000000000..c3a43828a --- /dev/null +++ b/sdk/base/lib/osBindings/ApiState.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ApiState = "error" | "initializing" | "running" diff --git a/sdk/base/lib/osBindings/AttachParams.ts b/sdk/base/lib/osBindings/AttachParams.ts new file mode 100644 index 000000000..048151d2f --- /dev/null +++ b/sdk/base/lib/osBindings/AttachParams.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" + +export type AttachParams = { + startOsPassword: EncryptedWire | null + guid: string +} diff --git a/sdk/base/lib/osBindings/BackupProgress.ts b/sdk/base/lib/osBindings/BackupProgress.ts new file mode 100644 index 000000000..407a4195a --- /dev/null +++ b/sdk/base/lib/osBindings/BackupProgress.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BackupProgress = { complete: boolean } diff --git a/sdk/base/lib/osBindings/BackupTargetFS.ts b/sdk/base/lib/osBindings/BackupTargetFS.ts new file mode 100644 index 000000000..0cff2cc4e --- /dev/null +++ b/sdk/base/lib/osBindings/BackupTargetFS.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BlockDev } from "./BlockDev" +import type { Cifs } from "./Cifs" + +export type BackupTargetFS = + | ({ type: "disk" } & BlockDev) + | ({ type: "cifs" } & Cifs) diff --git a/sdk/base/lib/osBindings/Base64.ts b/sdk/base/lib/osBindings/Base64.ts new file mode 100644 index 000000000..597227fc9 --- /dev/null +++ b/sdk/base/lib/osBindings/Base64.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Base64 = string diff --git a/sdk/base/lib/osBindings/BindId.ts b/sdk/base/lib/osBindings/BindId.ts new file mode 100644 index 000000000..778d95346 --- /dev/null +++ b/sdk/base/lib/osBindings/BindId.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostId } from "./HostId" + +export type BindId = { id: HostId; internalPort: number } diff --git a/sdk/base/lib/osBindings/BindInfo.ts b/sdk/base/lib/osBindings/BindInfo.ts new file mode 100644 index 000000000..b03dbe6b2 --- /dev/null +++ b/sdk/base/lib/osBindings/BindInfo.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindOptions } from "./BindOptions" +import type { NetInfo } from "./NetInfo" + +export type BindInfo = { enabled: boolean; options: BindOptions; net: NetInfo } diff --git a/sdk/base/lib/osBindings/BindOptions.ts b/sdk/base/lib/osBindings/BindOptions.ts new file mode 100644 index 000000000..49d9ecbf2 --- /dev/null +++ b/sdk/base/lib/osBindings/BindOptions.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddSslOptions } from "./AddSslOptions" +import type { Security } from "./Security" + +export type BindOptions = { + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: Security | null +} diff --git a/sdk/base/lib/osBindings/BindParams.ts b/sdk/base/lib/osBindings/BindParams.ts new file mode 100644 index 000000000..9064a33b1 --- /dev/null +++ b/sdk/base/lib/osBindings/BindParams.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddSslOptions } from "./AddSslOptions" +import type { HostId } from "./HostId" +import type { Security } from "./Security" + +export type BindParams = { + id: HostId + internalPort: number + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: Security | null +} diff --git a/sdk/base/lib/osBindings/BindingSetPublicParams.ts b/sdk/base/lib/osBindings/BindingSetPublicParams.ts new file mode 100644 index 000000000..077cf8510 --- /dev/null +++ b/sdk/base/lib/osBindings/BindingSetPublicParams.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BindingSetPublicParams = { + internalPort: number + public: boolean | null +} diff --git a/sdk/base/lib/osBindings/Blake3Commitment.ts b/sdk/base/lib/osBindings/Blake3Commitment.ts new file mode 100644 index 000000000..690559122 --- /dev/null +++ b/sdk/base/lib/osBindings/Blake3Commitment.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Base64 } from "./Base64" + +export type Blake3Commitment = { hash: Base64; size: number } diff --git a/sdk/base/lib/osBindings/BlockDev.ts b/sdk/base/lib/osBindings/BlockDev.ts new file mode 100644 index 000000000..46db81011 --- /dev/null +++ b/sdk/base/lib/osBindings/BlockDev.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BlockDev = { logicalname: string } diff --git a/sdk/base/lib/osBindings/BuildArg.ts b/sdk/base/lib/osBindings/BuildArg.ts new file mode 100644 index 000000000..1157828f5 --- /dev/null +++ b/sdk/base/lib/osBindings/BuildArg.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BuildArg = string | { env: string } diff --git a/sdk/base/lib/osBindings/CallbackId.ts b/sdk/base/lib/osBindings/CallbackId.ts new file mode 100644 index 000000000..0ac5d7ce2 --- /dev/null +++ b/sdk/base/lib/osBindings/CallbackId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CallbackId = number diff --git a/sdk/base/lib/osBindings/Category.ts b/sdk/base/lib/osBindings/Category.ts new file mode 100644 index 000000000..6e0815675 --- /dev/null +++ b/sdk/base/lib/osBindings/Category.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Description } from "./Description" + +export type Category = { name: string; description: Description } diff --git a/sdk/base/lib/osBindings/CheckDependenciesParam.ts b/sdk/base/lib/osBindings/CheckDependenciesParam.ts new file mode 100644 index 000000000..3a00faf4f --- /dev/null +++ b/sdk/base/lib/osBindings/CheckDependenciesParam.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" + +export type CheckDependenciesParam = { packageIds?: Array } diff --git a/sdk/base/lib/osBindings/CheckDependenciesResult.ts b/sdk/base/lib/osBindings/CheckDependenciesResult.ts new file mode 100644 index 000000000..3fa34b600 --- /dev/null +++ b/sdk/base/lib/osBindings/CheckDependenciesResult.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionRequestEntry } from "./ActionRequestEntry" +import type { HealthCheckId } from "./HealthCheckId" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" +import type { PackageId } from "./PackageId" +import type { ReplayId } from "./ReplayId" +import type { Version } from "./Version" + +export type CheckDependenciesResult = { + packageId: PackageId + title: string | null + installedVersion: Version | null + satisfies: Array + isRunning: boolean + requestedActions: { [key: ReplayId]: ActionRequestEntry } + healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult } +} diff --git a/sdk/base/lib/osBindings/Cifs.ts b/sdk/base/lib/osBindings/Cifs.ts new file mode 100644 index 000000000..f7099bd7f --- /dev/null +++ b/sdk/base/lib/osBindings/Cifs.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Cifs = { + hostname: string + path: string + username: string + password: string | null +} diff --git a/sdk/base/lib/osBindings/ClearActionRequestsParams.ts b/sdk/base/lib/osBindings/ClearActionRequestsParams.ts new file mode 100644 index 000000000..856a13de4 --- /dev/null +++ b/sdk/base/lib/osBindings/ClearActionRequestsParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClearActionRequestsParams = + | { only: string[] } + | { except: string[] } diff --git a/sdk/base/lib/osBindings/ClearActionsParams.ts b/sdk/base/lib/osBindings/ClearActionsParams.ts new file mode 100644 index 000000000..68cff676a --- /dev/null +++ b/sdk/base/lib/osBindings/ClearActionsParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" + +export type ClearActionsParams = { except: Array } diff --git a/sdk/base/lib/osBindings/ClearBindingsParams.ts b/sdk/base/lib/osBindings/ClearBindingsParams.ts new file mode 100644 index 000000000..41f1d5741 --- /dev/null +++ b/sdk/base/lib/osBindings/ClearBindingsParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindId } from "./BindId" + +export type ClearBindingsParams = { except: Array } diff --git a/sdk/base/lib/osBindings/ClearCallbacksParams.ts b/sdk/base/lib/osBindings/ClearCallbacksParams.ts new file mode 100644 index 000000000..095a27f5e --- /dev/null +++ b/sdk/base/lib/osBindings/ClearCallbacksParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClearCallbacksParams = { only: number[] } | { except: number[] } diff --git a/sdk/base/lib/osBindings/ClearServiceInterfacesParams.ts b/sdk/base/lib/osBindings/ClearServiceInterfacesParams.ts new file mode 100644 index 000000000..02c177978 --- /dev/null +++ b/sdk/base/lib/osBindings/ClearServiceInterfacesParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ServiceInterfaceId } from "./ServiceInterfaceId" + +export type ClearServiceInterfacesParams = { except: Array } diff --git a/sdk/base/lib/osBindings/CliSetIconParams.ts b/sdk/base/lib/osBindings/CliSetIconParams.ts new file mode 100644 index 000000000..4e47362b6 --- /dev/null +++ b/sdk/base/lib/osBindings/CliSetIconParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CliSetIconParams = { icon: string } diff --git a/sdk/base/lib/osBindings/ContactInfo.ts b/sdk/base/lib/osBindings/ContactInfo.ts new file mode 100644 index 000000000..e6d4c13b2 --- /dev/null +++ b/sdk/base/lib/osBindings/ContactInfo.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ContactInfo = + | { email: string } + | { matrix: string } + | { website: string } diff --git a/sdk/base/lib/osBindings/CreateSubcontainerFsParams.ts b/sdk/base/lib/osBindings/CreateSubcontainerFsParams.ts new file mode 100644 index 000000000..32d301e4a --- /dev/null +++ b/sdk/base/lib/osBindings/CreateSubcontainerFsParams.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageId } from "./ImageId" + +export type CreateSubcontainerFsParams = { + imageId: ImageId + name: string | null +} diff --git a/sdk/base/lib/osBindings/CurrentDependencies.ts b/sdk/base/lib/osBindings/CurrentDependencies.ts new file mode 100644 index 000000000..029a2f018 --- /dev/null +++ b/sdk/base/lib/osBindings/CurrentDependencies.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CurrentDependencyInfo } from "./CurrentDependencyInfo" +import type { PackageId } from "./PackageId" + +export type CurrentDependencies = { [key: PackageId]: CurrentDependencyInfo } diff --git a/sdk/base/lib/osBindings/CurrentDependencyInfo.ts b/sdk/base/lib/osBindings/CurrentDependencyInfo.ts new file mode 100644 index 000000000..e56e2be7a --- /dev/null +++ b/sdk/base/lib/osBindings/CurrentDependencyInfo.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" + +export type CurrentDependencyInfo = { + title: string | null + icon: DataUrl | null + versionRange: string +} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) diff --git a/sdk/base/lib/osBindings/DataUrl.ts b/sdk/base/lib/osBindings/DataUrl.ts new file mode 100644 index 000000000..cf79cb4a4 --- /dev/null +++ b/sdk/base/lib/osBindings/DataUrl.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DataUrl = string diff --git a/sdk/base/lib/osBindings/DepInfo.ts b/sdk/base/lib/osBindings/DepInfo.ts new file mode 100644 index 000000000..d635cca3b --- /dev/null +++ b/sdk/base/lib/osBindings/DepInfo.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PathOrUrl } from "./PathOrUrl" + +export type DepInfo = { + description: string | null + optional: boolean + s9pk: PathOrUrl | null +} diff --git a/sdk/base/lib/osBindings/Dependencies.ts b/sdk/base/lib/osBindings/Dependencies.ts new file mode 100644 index 000000000..ad4c9b745 --- /dev/null +++ b/sdk/base/lib/osBindings/Dependencies.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DepInfo } from "./DepInfo" +import type { PackageId } from "./PackageId" + +export type Dependencies = { [key: PackageId]: DepInfo } diff --git a/sdk/base/lib/osBindings/DependencyKind.ts b/sdk/base/lib/osBindings/DependencyKind.ts new file mode 100644 index 000000000..e7021ba16 --- /dev/null +++ b/sdk/base/lib/osBindings/DependencyKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DependencyKind = "exists" | "running" diff --git a/sdk/base/lib/osBindings/DependencyMetadata.ts b/sdk/base/lib/osBindings/DependencyMetadata.ts new file mode 100644 index 000000000..3d56ef052 --- /dev/null +++ b/sdk/base/lib/osBindings/DependencyMetadata.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" + +export type DependencyMetadata = { + title: string | null + icon: DataUrl | null + description: string | null + optional: boolean +} diff --git a/sdk/base/lib/osBindings/DependencyRequirement.ts b/sdk/base/lib/osBindings/DependencyRequirement.ts new file mode 100644 index 000000000..3b857c476 --- /dev/null +++ b/sdk/base/lib/osBindings/DependencyRequirement.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" +import type { PackageId } from "./PackageId" + +export type DependencyRequirement = + | { + kind: "running" + id: PackageId + healthChecks: Array + versionRange: string + } + | { kind: "exists"; id: PackageId; versionRange: string } diff --git a/sdk/base/lib/osBindings/Description.ts b/sdk/base/lib/osBindings/Description.ts new file mode 100644 index 000000000..bcb92071f --- /dev/null +++ b/sdk/base/lib/osBindings/Description.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Description = { short: string; long: string } diff --git a/sdk/base/lib/osBindings/DestroySubcontainerFsParams.ts b/sdk/base/lib/osBindings/DestroySubcontainerFsParams.ts new file mode 100644 index 000000000..3f85d2217 --- /dev/null +++ b/sdk/base/lib/osBindings/DestroySubcontainerFsParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" + +export type DestroySubcontainerFsParams = { guid: Guid } diff --git a/sdk/base/lib/osBindings/DeviceFilter.ts b/sdk/base/lib/osBindings/DeviceFilter.ts new file mode 100644 index 000000000..6e6f5810c --- /dev/null +++ b/sdk/base/lib/osBindings/DeviceFilter.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DeviceFilter = { + class: "processor" | "display" + pattern: string + patternDescription: string +} diff --git a/sdk/base/lib/osBindings/DomainConfig.ts b/sdk/base/lib/osBindings/DomainConfig.ts new file mode 100644 index 000000000..433bc65f5 --- /dev/null +++ b/sdk/base/lib/osBindings/DomainConfig.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AcmeProvider } from "./AcmeProvider" + +export type DomainConfig = { public: boolean; acme: AcmeProvider | null } diff --git a/sdk/base/lib/osBindings/Duration.ts b/sdk/base/lib/osBindings/Duration.ts new file mode 100644 index 000000000..b44758a28 --- /dev/null +++ b/sdk/base/lib/osBindings/Duration.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Duration = string diff --git a/sdk/base/lib/osBindings/EchoParams.ts b/sdk/base/lib/osBindings/EchoParams.ts new file mode 100644 index 000000000..232dfb8ab --- /dev/null +++ b/sdk/base/lib/osBindings/EchoParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EchoParams = { message: string } diff --git a/sdk/base/lib/osBindings/EditSignerParams.ts b/sdk/base/lib/osBindings/EditSignerParams.ts new file mode 100644 index 000000000..9532709a6 --- /dev/null +++ b/sdk/base/lib/osBindings/EditSignerParams.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnyVerifyingKey } from "./AnyVerifyingKey" +import type { ContactInfo } from "./ContactInfo" +import type { Guid } from "./Guid" + +export type EditSignerParams = { + id: Guid + setName: string | null + addContact: Array + addKey: Array + removeContact: Array + removeKey: Array +} diff --git a/sdk/base/lib/osBindings/EncryptedWire.ts b/sdk/base/lib/osBindings/EncryptedWire.ts new file mode 100644 index 000000000..16a2f95b8 --- /dev/null +++ b/sdk/base/lib/osBindings/EncryptedWire.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EncryptedWire = { encrypted: any } diff --git a/sdk/base/lib/osBindings/ExportActionParams.ts b/sdk/base/lib/osBindings/ExportActionParams.ts new file mode 100644 index 000000000..d4aa33102 --- /dev/null +++ b/sdk/base/lib/osBindings/ExportActionParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { ActionMetadata } from "./ActionMetadata" + +export type ExportActionParams = { id: ActionId; metadata: ActionMetadata } diff --git a/sdk/base/lib/osBindings/ExportServiceInterfaceParams.ts b/sdk/base/lib/osBindings/ExportServiceInterfaceParams.ts new file mode 100644 index 000000000..675c3e06d --- /dev/null +++ b/sdk/base/lib/osBindings/ExportServiceInterfaceParams.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { ServiceInterfaceType } from "./ServiceInterfaceType" + +export type ExportServiceInterfaceParams = { + id: ServiceInterfaceId + name: string + description: string + masked: boolean + addressInfo: AddressInfo + type: ServiceInterfaceType +} diff --git a/sdk/base/lib/osBindings/ExposeForDependentsParams.ts b/sdk/base/lib/osBindings/ExposeForDependentsParams.ts new file mode 100644 index 000000000..5b55368b9 --- /dev/null +++ b/sdk/base/lib/osBindings/ExposeForDependentsParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExposeForDependentsParams = { paths: string[] } diff --git a/sdk/base/lib/osBindings/ForgetInterfaceParams.ts b/sdk/base/lib/osBindings/ForgetInterfaceParams.ts new file mode 100644 index 000000000..b3532602c --- /dev/null +++ b/sdk/base/lib/osBindings/ForgetInterfaceParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ForgetInterfaceParams = { interface: string } diff --git a/sdk/base/lib/osBindings/FullIndex.ts b/sdk/base/lib/osBindings/FullIndex.ts new file mode 100644 index 000000000..c7889760a --- /dev/null +++ b/sdk/base/lib/osBindings/FullIndex.ts @@ -0,0 +1,14 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" +import type { Guid } from "./Guid" +import type { OsIndex } from "./OsIndex" +import type { PackageIndex } from "./PackageIndex" +import type { SignerInfo } from "./SignerInfo" + +export type FullIndex = { + name: string | null + icon: DataUrl | null + package: PackageIndex + os: OsIndex + signers: { [key: Guid]: SignerInfo } +} diff --git a/sdk/base/lib/osBindings/FullProgress.ts b/sdk/base/lib/osBindings/FullProgress.ts new file mode 100644 index 000000000..8961ea0e7 --- /dev/null +++ b/sdk/base/lib/osBindings/FullProgress.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NamedProgress } from "./NamedProgress" +import type { Progress } from "./Progress" + +export type FullProgress = { overall: Progress; phases: Array } diff --git a/sdk/base/lib/osBindings/GetActionInputParams.ts b/sdk/base/lib/osBindings/GetActionInputParams.ts new file mode 100644 index 000000000..568ceb907 --- /dev/null +++ b/sdk/base/lib/osBindings/GetActionInputParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { PackageId } from "./PackageId" + +export type GetActionInputParams = { packageId?: PackageId; actionId: ActionId } diff --git a/sdk/base/lib/osBindings/GetHostInfoParams.ts b/sdk/base/lib/osBindings/GetHostInfoParams.ts new file mode 100644 index 000000000..ff6d9d709 --- /dev/null +++ b/sdk/base/lib/osBindings/GetHostInfoParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" + +export type GetHostInfoParams = { + hostId: HostId + packageId?: PackageId + callback?: CallbackId +} diff --git a/sdk/base/lib/osBindings/GetOsAssetParams.ts b/sdk/base/lib/osBindings/GetOsAssetParams.ts new file mode 100644 index 000000000..100f711c7 --- /dev/null +++ b/sdk/base/lib/osBindings/GetOsAssetParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GetOsAssetParams = { version: string; platform: string } diff --git a/sdk/base/lib/osBindings/GetOsVersionParams.ts b/sdk/base/lib/osBindings/GetOsVersionParams.ts new file mode 100644 index 000000000..de0458645 --- /dev/null +++ b/sdk/base/lib/osBindings/GetOsVersionParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GetOsVersionParams = { + source: string | null + target: string | null + serverId: string | null + arch: string | null +} diff --git a/sdk/base/lib/osBindings/GetPackageParams.ts b/sdk/base/lib/osBindings/GetPackageParams.ts new file mode 100644 index 000000000..3dde55b28 --- /dev/null +++ b/sdk/base/lib/osBindings/GetPackageParams.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageDetailLevel } from "./PackageDetailLevel" +import type { PackageId } from "./PackageId" +import type { Version } from "./Version" + +export type GetPackageParams = { + id: PackageId | null + version: string | null + sourceVersion: Version | null + otherVersions: PackageDetailLevel +} diff --git a/sdk/base/lib/osBindings/GetPackageResponse.ts b/sdk/base/lib/osBindings/GetPackageResponse.ts new file mode 100644 index 000000000..3e1dd4e9d --- /dev/null +++ b/sdk/base/lib/osBindings/GetPackageResponse.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageInfoShort } from "./PackageInfoShort" +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type GetPackageResponse = { + categories: string[] + best: { [key: Version]: PackageVersionInfo } + otherVersions?: { [key: Version]: PackageInfoShort } +} diff --git a/sdk/base/lib/osBindings/GetPackageResponseFull.ts b/sdk/base/lib/osBindings/GetPackageResponseFull.ts new file mode 100644 index 000000000..e375dd489 --- /dev/null +++ b/sdk/base/lib/osBindings/GetPackageResponseFull.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type GetPackageResponseFull = { + categories: string[] + best: { [key: Version]: PackageVersionInfo } + otherVersions: { [key: Version]: PackageVersionInfo } +} diff --git a/sdk/base/lib/osBindings/GetServiceInterfaceParams.ts b/sdk/base/lib/osBindings/GetServiceInterfaceParams.ts new file mode 100644 index 000000000..b71591e17 --- /dev/null +++ b/sdk/base/lib/osBindings/GetServiceInterfaceParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" + +export type GetServiceInterfaceParams = { + packageId?: PackageId + serviceInterfaceId: ServiceInterfaceId + callback?: CallbackId +} diff --git a/sdk/base/lib/osBindings/GetServicePortForwardParams.ts b/sdk/base/lib/osBindings/GetServicePortForwardParams.ts new file mode 100644 index 000000000..63236328e --- /dev/null +++ b/sdk/base/lib/osBindings/GetServicePortForwardParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" + +export type GetServicePortForwardParams = { + packageId?: PackageId + hostId: HostId + internalPort: number +} diff --git a/sdk/base/lib/osBindings/GetSslCertificateParams.ts b/sdk/base/lib/osBindings/GetSslCertificateParams.ts new file mode 100644 index 000000000..85c677540 --- /dev/null +++ b/sdk/base/lib/osBindings/GetSslCertificateParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Algorithm } from "./Algorithm" +import type { CallbackId } from "./CallbackId" + +export type GetSslCertificateParams = { + hostnames: string[] + algorithm?: Algorithm + callback?: CallbackId +} diff --git a/sdk/base/lib/osBindings/GetSslKeyParams.ts b/sdk/base/lib/osBindings/GetSslKeyParams.ts new file mode 100644 index 000000000..2ca3076c8 --- /dev/null +++ b/sdk/base/lib/osBindings/GetSslKeyParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Algorithm } from "./Algorithm" + +export type GetSslKeyParams = { hostnames: string[]; algorithm?: Algorithm } diff --git a/sdk/base/lib/osBindings/GetStatusParams.ts b/sdk/base/lib/osBindings/GetStatusParams.ts new file mode 100644 index 000000000..a0fbe3bfc --- /dev/null +++ b/sdk/base/lib/osBindings/GetStatusParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" + +export type GetStatusParams = { packageId?: PackageId; callback?: CallbackId } diff --git a/sdk/base/lib/osBindings/GetStoreParams.ts b/sdk/base/lib/osBindings/GetStoreParams.ts new file mode 100644 index 000000000..e134cd4a6 --- /dev/null +++ b/sdk/base/lib/osBindings/GetStoreParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" + +export type GetStoreParams = { + packageId?: PackageId + path: string + callback?: CallbackId +} diff --git a/sdk/base/lib/osBindings/GetSystemSmtpParams.ts b/sdk/base/lib/osBindings/GetSystemSmtpParams.ts new file mode 100644 index 000000000..73b91057c --- /dev/null +++ b/sdk/base/lib/osBindings/GetSystemSmtpParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" + +export type GetSystemSmtpParams = { callback: CallbackId | null } diff --git a/sdk/base/lib/osBindings/GitHash.ts b/sdk/base/lib/osBindings/GitHash.ts new file mode 100644 index 000000000..43f6adde3 --- /dev/null +++ b/sdk/base/lib/osBindings/GitHash.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GitHash = string diff --git a/sdk/base/lib/osBindings/Governor.ts b/sdk/base/lib/osBindings/Governor.ts new file mode 100644 index 000000000..25e5f757f --- /dev/null +++ b/sdk/base/lib/osBindings/Governor.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Governor = string diff --git a/sdk/base/lib/osBindings/Guid.ts b/sdk/base/lib/osBindings/Guid.ts new file mode 100644 index 000000000..28dc4d92d --- /dev/null +++ b/sdk/base/lib/osBindings/Guid.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Guid = string diff --git a/sdk/base/lib/osBindings/HardwareRequirements.ts b/sdk/base/lib/osBindings/HardwareRequirements.ts new file mode 100644 index 000000000..d420f846b --- /dev/null +++ b/sdk/base/lib/osBindings/HardwareRequirements.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DeviceFilter } from "./DeviceFilter" + +export type HardwareRequirements = { + device: Array + ram: number | null + arch: string[] | null +} diff --git a/sdk/base/lib/osBindings/HealthCheckId.ts b/sdk/base/lib/osBindings/HealthCheckId.ts new file mode 100644 index 000000000..bc5300473 --- /dev/null +++ b/sdk/base/lib/osBindings/HealthCheckId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HealthCheckId = string diff --git a/sdk/base/lib/osBindings/Host.ts b/sdk/base/lib/osBindings/Host.ts new file mode 100644 index 000000000..041e1c9bc --- /dev/null +++ b/sdk/base/lib/osBindings/Host.ts @@ -0,0 +1,14 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindInfo } from "./BindInfo" +import type { DomainConfig } from "./DomainConfig" +import type { HostnameInfo } from "./HostnameInfo" + +export type Host = { + bindings: { [key: number]: BindInfo } + onions: string[] + domains: { [key: string]: DomainConfig } + /** + * COMPUTED: NetService::update + */ + hostnameInfo: { [key: number]: Array } +} diff --git a/sdk/base/lib/osBindings/HostAddress.ts b/sdk/base/lib/osBindings/HostAddress.ts new file mode 100644 index 000000000..fe16c89d7 --- /dev/null +++ b/sdk/base/lib/osBindings/HostAddress.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AcmeProvider } from "./AcmeProvider" + +export type HostAddress = + | { kind: "onion"; address: string } + | { + kind: "domain" + address: string + public: boolean + acme: AcmeProvider | null + } diff --git a/sdk/base/lib/osBindings/HostId.ts b/sdk/base/lib/osBindings/HostId.ts new file mode 100644 index 000000000..f18dc2498 --- /dev/null +++ b/sdk/base/lib/osBindings/HostId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HostId = string diff --git a/sdk/base/lib/osBindings/HostnameInfo.ts b/sdk/base/lib/osBindings/HostnameInfo.ts new file mode 100644 index 000000000..ef8bafac0 --- /dev/null +++ b/sdk/base/lib/osBindings/HostnameInfo.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { IpHostname } from "./IpHostname" +import type { OnionHostname } from "./OnionHostname" + +export type HostnameInfo = + | { + kind: "ip" + networkInterfaceId: string + public: boolean + hostname: IpHostname + } + | { kind: "onion"; hostname: OnionHostname } diff --git a/sdk/base/lib/osBindings/Hosts.ts b/sdk/base/lib/osBindings/Hosts.ts new file mode 100644 index 000000000..c7aa84996 --- /dev/null +++ b/sdk/base/lib/osBindings/Hosts.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Host } from "./Host" +import type { HostId } from "./HostId" + +export type Hosts = { [key: HostId]: Host } diff --git a/sdk/base/lib/osBindings/ImageConfig.ts b/sdk/base/lib/osBindings/ImageConfig.ts new file mode 100644 index 000000000..2b1033b83 --- /dev/null +++ b/sdk/base/lib/osBindings/ImageConfig.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageSource } from "./ImageSource" + +export type ImageConfig = { + source: ImageSource + arch: string[] + emulateMissingAs: string | null +} diff --git a/sdk/base/lib/osBindings/ImageId.ts b/sdk/base/lib/osBindings/ImageId.ts new file mode 100644 index 000000000..330b812b9 --- /dev/null +++ b/sdk/base/lib/osBindings/ImageId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageId = string diff --git a/sdk/base/lib/osBindings/ImageMetadata.ts b/sdk/base/lib/osBindings/ImageMetadata.ts new file mode 100644 index 000000000..b50f7a084 --- /dev/null +++ b/sdk/base/lib/osBindings/ImageMetadata.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageMetadata = { workdir: string; user: string } diff --git a/sdk/base/lib/osBindings/ImageSource.ts b/sdk/base/lib/osBindings/ImageSource.ts new file mode 100644 index 000000000..d8f876aef --- /dev/null +++ b/sdk/base/lib/osBindings/ImageSource.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BuildArg } from "./BuildArg" + +export type ImageSource = + | "packed" + | { + dockerBuild: { + workdir?: string + dockerfile?: string + buildArgs?: { [key: string]: BuildArg } + } + } + | { dockerTag: string } diff --git a/sdk/base/lib/osBindings/InitProgressRes.ts b/sdk/base/lib/osBindings/InitProgressRes.ts new file mode 100644 index 000000000..38caf7bdb --- /dev/null +++ b/sdk/base/lib/osBindings/InitProgressRes.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FullProgress } from "./FullProgress" +import type { Guid } from "./Guid" + +export type InitProgressRes = { progress: FullProgress; guid: Guid } diff --git a/sdk/base/lib/osBindings/InstallParams.ts b/sdk/base/lib/osBindings/InstallParams.ts new file mode 100644 index 000000000..2b70ad593 --- /dev/null +++ b/sdk/base/lib/osBindings/InstallParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" +import type { Version } from "./Version" + +export type InstallParams = { + registry: string + id: PackageId + version: Version +} diff --git a/sdk/base/lib/osBindings/InstalledState.ts b/sdk/base/lib/osBindings/InstalledState.ts new file mode 100644 index 000000000..48177287e --- /dev/null +++ b/sdk/base/lib/osBindings/InstalledState.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Manifest } from "./Manifest" + +export type InstalledState = { manifest: Manifest } diff --git a/sdk/base/lib/osBindings/InstalledVersionParams.ts b/sdk/base/lib/osBindings/InstalledVersionParams.ts new file mode 100644 index 000000000..637f9b6ce --- /dev/null +++ b/sdk/base/lib/osBindings/InstalledVersionParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" + +export type InstalledVersionParams = { id: PackageId } diff --git a/sdk/base/lib/osBindings/InstallingInfo.ts b/sdk/base/lib/osBindings/InstallingInfo.ts new file mode 100644 index 000000000..6b77b49d9 --- /dev/null +++ b/sdk/base/lib/osBindings/InstallingInfo.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FullProgress } from "./FullProgress" +import type { Manifest } from "./Manifest" + +export type InstallingInfo = { newManifest: Manifest; progress: FullProgress } diff --git a/sdk/base/lib/osBindings/InstallingState.ts b/sdk/base/lib/osBindings/InstallingState.ts new file mode 100644 index 000000000..db752df90 --- /dev/null +++ b/sdk/base/lib/osBindings/InstallingState.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstallingInfo } from "./InstallingInfo" + +export type InstallingState = { installingInfo: InstallingInfo } diff --git a/sdk/base/lib/osBindings/IpHostname.ts b/sdk/base/lib/osBindings/IpHostname.ts new file mode 100644 index 000000000..9b3ddd6d1 --- /dev/null +++ b/sdk/base/lib/osBindings/IpHostname.ts @@ -0,0 +1,24 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type IpHostname = + | { kind: "ipv4"; value: string; port: number | null; sslPort: number | null } + | { + kind: "ipv6" + value: string + scopeId: number + port: number | null + sslPort: number | null + } + | { + kind: "local" + value: string + port: number | null + sslPort: number | null + } + | { + kind: "domain" + domain: string + subdomain: string | null + port: number | null + sslPort: number | null + } diff --git a/sdk/base/lib/osBindings/IpInfo.ts b/sdk/base/lib/osBindings/IpInfo.ts new file mode 100644 index 000000000..260add9e6 --- /dev/null +++ b/sdk/base/lib/osBindings/IpInfo.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkInterfaceType } from "./NetworkInterfaceType" + +export type IpInfo = { + scopeId: number + deviceType: NetworkInterfaceType | null + subnets: string[] + wanIp: string | null + ntpServers: string[] +} diff --git a/sdk/base/lib/osBindings/ListPackageSignersParams.ts b/sdk/base/lib/osBindings/ListPackageSignersParams.ts new file mode 100644 index 000000000..73cd6a745 --- /dev/null +++ b/sdk/base/lib/osBindings/ListPackageSignersParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" + +export type ListPackageSignersParams = { id: PackageId } diff --git a/sdk/base/lib/osBindings/ListServiceInterfacesParams.ts b/sdk/base/lib/osBindings/ListServiceInterfacesParams.ts new file mode 100644 index 000000000..fd27ace2b --- /dev/null +++ b/sdk/base/lib/osBindings/ListServiceInterfacesParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" + +export type ListServiceInterfacesParams = { + packageId?: PackageId + callback?: CallbackId +} diff --git a/sdk/base/lib/osBindings/ListVersionSignersParams.ts b/sdk/base/lib/osBindings/ListVersionSignersParams.ts new file mode 100644 index 000000000..baf516bf2 --- /dev/null +++ b/sdk/base/lib/osBindings/ListVersionSignersParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ListVersionSignersParams = { version: string } diff --git a/sdk/base/lib/osBindings/LoginParams.ts b/sdk/base/lib/osBindings/LoginParams.ts new file mode 100644 index 000000000..acaf5b8a1 --- /dev/null +++ b/sdk/base/lib/osBindings/LoginParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PasswordType } from "./PasswordType" + +export type LoginParams = { + password: PasswordType | null + ephemeral: boolean + metadata: any +} diff --git a/sdk/base/lib/osBindings/LshwDevice.ts b/sdk/base/lib/osBindings/LshwDevice.ts new file mode 100644 index 000000000..f2c624be7 --- /dev/null +++ b/sdk/base/lib/osBindings/LshwDevice.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LshwDisplay } from "./LshwDisplay" +import type { LshwProcessor } from "./LshwProcessor" + +export type LshwDevice = + | ({ class: "processor" } & LshwProcessor) + | ({ class: "display" } & LshwDisplay) diff --git a/sdk/base/lib/osBindings/LshwDisplay.ts b/sdk/base/lib/osBindings/LshwDisplay.ts new file mode 100644 index 000000000..25d14b12f --- /dev/null +++ b/sdk/base/lib/osBindings/LshwDisplay.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LshwDisplay = { product: string } diff --git a/sdk/base/lib/osBindings/LshwProcessor.ts b/sdk/base/lib/osBindings/LshwProcessor.ts new file mode 100644 index 000000000..17a47d477 --- /dev/null +++ b/sdk/base/lib/osBindings/LshwProcessor.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LshwProcessor = { product: string } diff --git a/sdk/base/lib/osBindings/MainStatus.ts b/sdk/base/lib/osBindings/MainStatus.ts new file mode 100644 index 000000000..64e081ab9 --- /dev/null +++ b/sdk/base/lib/osBindings/MainStatus.ts @@ -0,0 +1,26 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" +import type { StartStop } from "./StartStop" + +export type MainStatus = + | { + main: "error" + onRebuild: StartStop + message: string + debug: string | null + } + | { main: "stopped" } + | { main: "restarting" } + | { main: "restoring" } + | { main: "stopping" } + | { + main: "starting" + health: { [key: HealthCheckId]: NamedHealthCheckResult } + } + | { + main: "running" + started: string + health: { [key: HealthCheckId]: NamedHealthCheckResult } + } + | { main: "backingUp"; onComplete: StartStop } diff --git a/sdk/base/lib/osBindings/Manifest.ts b/sdk/base/lib/osBindings/Manifest.ts new file mode 100644 index 000000000..2c9a2457e --- /dev/null +++ b/sdk/base/lib/osBindings/Manifest.ts @@ -0,0 +1,36 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Alerts } from "./Alerts" +import type { Dependencies } from "./Dependencies" +import type { Description } from "./Description" +import type { GitHash } from "./GitHash" +import type { HardwareRequirements } from "./HardwareRequirements" +import type { ImageConfig } from "./ImageConfig" +import type { ImageId } from "./ImageId" +import type { PackageId } from "./PackageId" +import type { Version } from "./Version" +import type { VolumeId } from "./VolumeId" + +export type Manifest = { + id: PackageId + title: string + version: Version + satisfies: Array + releaseNotes: string + canMigrateTo: string + canMigrateFrom: string + license: string + wrapperRepo: string + upstreamRepo: string + supportSite: string + marketingSite: string + donationUrl: string | null + description: Description + images: { [key: ImageId]: ImageConfig } + assets: Array + volumes: Array + alerts: Alerts + dependencies: Dependencies + hardwareRequirements: HardwareRequirements + gitHash?: GitHash + osVersion: string +} diff --git a/sdk/base/lib/osBindings/MaybeUtf8String.ts b/sdk/base/lib/osBindings/MaybeUtf8String.ts new file mode 100644 index 000000000..9532e9b97 --- /dev/null +++ b/sdk/base/lib/osBindings/MaybeUtf8String.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MaybeUtf8String = string | number[] diff --git a/sdk/base/lib/osBindings/MerkleArchiveCommitment.ts b/sdk/base/lib/osBindings/MerkleArchiveCommitment.ts new file mode 100644 index 000000000..5dcaa0aa3 --- /dev/null +++ b/sdk/base/lib/osBindings/MerkleArchiveCommitment.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Base64 } from "./Base64" + +export type MerkleArchiveCommitment = { + rootSighash: Base64 + rootMaxsize: number +} diff --git a/sdk/base/lib/osBindings/MountParams.ts b/sdk/base/lib/osBindings/MountParams.ts new file mode 100644 index 000000000..778983fd6 --- /dev/null +++ b/sdk/base/lib/osBindings/MountParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MountTarget } from "./MountTarget" + +export type MountParams = { location: string; target: MountTarget } diff --git a/sdk/base/lib/osBindings/MountTarget.ts b/sdk/base/lib/osBindings/MountTarget.ts new file mode 100644 index 000000000..bbee5453b --- /dev/null +++ b/sdk/base/lib/osBindings/MountTarget.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" +import type { VolumeId } from "./VolumeId" + +export type MountTarget = { + packageId: PackageId + volumeId: VolumeId + subpath: string | null + readonly: boolean +} diff --git a/sdk/base/lib/osBindings/NamedHealthCheckResult.ts b/sdk/base/lib/osBindings/NamedHealthCheckResult.ts new file mode 100644 index 000000000..c967e9b34 --- /dev/null +++ b/sdk/base/lib/osBindings/NamedHealthCheckResult.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NamedHealthCheckResult = { name: string } & ( + | { result: "success"; message: string | null } + | { result: "disabled"; message: string | null } + | { result: "starting"; message: string | null } + | { result: "loading"; message: string } + | { result: "failure"; message: string } +) diff --git a/sdk/base/lib/osBindings/NamedProgress.ts b/sdk/base/lib/osBindings/NamedProgress.ts new file mode 100644 index 000000000..52a410a51 --- /dev/null +++ b/sdk/base/lib/osBindings/NamedProgress.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Progress } from "./Progress" + +export type NamedProgress = { name: string; progress: Progress } diff --git a/sdk/base/lib/osBindings/NetInfo.ts b/sdk/base/lib/osBindings/NetInfo.ts new file mode 100644 index 000000000..e790cadaa --- /dev/null +++ b/sdk/base/lib/osBindings/NetInfo.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetInfo = { + public: boolean + assignedPort: number | null + assignedSslPort: number | null +} diff --git a/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts new file mode 100644 index 000000000..796046b93 --- /dev/null +++ b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { IpInfo } from "./IpInfo" + +export type NetworkInterfaceInfo = { + public: boolean | null + ipInfo: IpInfo | null +} diff --git a/sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts b/sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts new file mode 100644 index 000000000..516bfc817 --- /dev/null +++ b/sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkInterfaceSetPublicParams = { + interface: string + public: boolean | null +} diff --git a/sdk/base/lib/osBindings/NetworkInterfaceType.ts b/sdk/base/lib/osBindings/NetworkInterfaceType.ts new file mode 100644 index 000000000..e20067dcc --- /dev/null +++ b/sdk/base/lib/osBindings/NetworkInterfaceType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkInterfaceType = "ethernet" | "wireless" | "wireguard" diff --git a/sdk/base/lib/osBindings/OnionHostname.ts b/sdk/base/lib/osBindings/OnionHostname.ts new file mode 100644 index 000000000..0bea8245e --- /dev/null +++ b/sdk/base/lib/osBindings/OnionHostname.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type OnionHostname = { + value: string + port: number | null + sslPort: number | null +} diff --git a/sdk/base/lib/osBindings/OsIndex.ts b/sdk/base/lib/osBindings/OsIndex.ts new file mode 100644 index 000000000..fe9a4e395 --- /dev/null +++ b/sdk/base/lib/osBindings/OsIndex.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { OsVersionInfoMap } from "./OsVersionInfoMap" + +export type OsIndex = { versions: OsVersionInfoMap } diff --git a/sdk/base/lib/osBindings/OsVersionInfo.ts b/sdk/base/lib/osBindings/OsVersionInfo.ts new file mode 100644 index 000000000..a88115350 --- /dev/null +++ b/sdk/base/lib/osBindings/OsVersionInfo.ts @@ -0,0 +1,14 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Blake3Commitment } from "./Blake3Commitment" +import type { Guid } from "./Guid" +import type { RegistryAsset } from "./RegistryAsset" + +export type OsVersionInfo = { + headline: string + releaseNotes: string + sourceVersion: string + authorized: Array + iso: { [key: string]: RegistryAsset } + squashfs: { [key: string]: RegistryAsset } + img: { [key: string]: RegistryAsset } +} diff --git a/sdk/base/lib/osBindings/OsVersionInfoMap.ts b/sdk/base/lib/osBindings/OsVersionInfoMap.ts new file mode 100644 index 000000000..6f333f1fb --- /dev/null +++ b/sdk/base/lib/osBindings/OsVersionInfoMap.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { OsVersionInfo } from "./OsVersionInfo" + +export type OsVersionInfoMap = { [key: string]: OsVersionInfo } diff --git a/sdk/base/lib/osBindings/PackageDataEntry.ts b/sdk/base/lib/osBindings/PackageDataEntry.ts new file mode 100644 index 000000000..86df7b767 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageDataEntry.ts @@ -0,0 +1,28 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { ActionMetadata } from "./ActionMetadata" +import type { ActionRequestEntry } from "./ActionRequestEntry" +import type { CurrentDependencies } from "./CurrentDependencies" +import type { DataUrl } from "./DataUrl" +import type { Hosts } from "./Hosts" +import type { MainStatus } from "./MainStatus" +import type { PackageState } from "./PackageState" +import type { ServiceInterface } from "./ServiceInterface" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { Version } from "./Version" + +export type PackageDataEntry = { + stateInfo: PackageState + dataVersion: Version | null + status: MainStatus + registry: string | null + developerKey: string + icon: DataUrl + lastBackup: string | null + currentDependencies: CurrentDependencies + actions: { [key: ActionId]: ActionMetadata } + requestedActions: { [key: string]: ActionRequestEntry } + serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterface } + hosts: Hosts + storeExposedDependents: string[] +} diff --git a/sdk/base/lib/osBindings/PackageDetailLevel.ts b/sdk/base/lib/osBindings/PackageDetailLevel.ts new file mode 100644 index 000000000..f2016f632 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageDetailLevel.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PackageDetailLevel = "none" | "short" | "full" diff --git a/sdk/base/lib/osBindings/PackageId.ts b/sdk/base/lib/osBindings/PackageId.ts new file mode 100644 index 000000000..9e3403303 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PackageId = string diff --git a/sdk/base/lib/osBindings/PackageIndex.ts b/sdk/base/lib/osBindings/PackageIndex.ts new file mode 100644 index 000000000..5e8c94945 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageIndex.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Category } from "./Category" +import type { PackageId } from "./PackageId" +import type { PackageInfo } from "./PackageInfo" + +export type PackageIndex = { + categories: { [key: string]: Category } + packages: { [key: PackageId]: PackageInfo } +} diff --git a/sdk/base/lib/osBindings/PackageInfo.ts b/sdk/base/lib/osBindings/PackageInfo.ts new file mode 100644 index 000000000..6d07cd43e --- /dev/null +++ b/sdk/base/lib/osBindings/PackageInfo.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type PackageInfo = { + authorized: Array + versions: { [key: Version]: PackageVersionInfo } + categories: string[] +} diff --git a/sdk/base/lib/osBindings/PackageInfoShort.ts b/sdk/base/lib/osBindings/PackageInfoShort.ts new file mode 100644 index 000000000..22c7fbea4 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageInfoShort.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PackageInfoShort = { releaseNotes: string } diff --git a/sdk/base/lib/osBindings/PackageSignerParams.ts b/sdk/base/lib/osBindings/PackageSignerParams.ts new file mode 100644 index 000000000..b131e3e23 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageSignerParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" +import type { PackageId } from "./PackageId" + +export type PackageSignerParams = { id: PackageId; signer: Guid } diff --git a/sdk/base/lib/osBindings/PackageState.ts b/sdk/base/lib/osBindings/PackageState.ts new file mode 100644 index 000000000..fd15076ca --- /dev/null +++ b/sdk/base/lib/osBindings/PackageState.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstalledState } from "./InstalledState" +import type { InstallingState } from "./InstallingState" +import type { UpdatingState } from "./UpdatingState" + +export type PackageState = + | ({ state: "installing" } & InstallingState) + | ({ state: "restoring" } & InstallingState) + | ({ state: "updating" } & UpdatingState) + | ({ state: "installed" } & InstalledState) + | ({ state: "removing" } & InstalledState) diff --git a/sdk/base/lib/osBindings/PackageVersionInfo.ts b/sdk/base/lib/osBindings/PackageVersionInfo.ts new file mode 100644 index 000000000..c71fd5921 --- /dev/null +++ b/sdk/base/lib/osBindings/PackageVersionInfo.ts @@ -0,0 +1,30 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Alerts } from "./Alerts" +import type { DataUrl } from "./DataUrl" +import type { DependencyMetadata } from "./DependencyMetadata" +import type { Description } from "./Description" +import type { GitHash } from "./GitHash" +import type { HardwareRequirements } from "./HardwareRequirements" +import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +import type { PackageId } from "./PackageId" +import type { RegistryAsset } from "./RegistryAsset" + +export type PackageVersionInfo = { + title: string + icon: DataUrl + description: Description + releaseNotes: string + gitHash: GitHash + license: string + wrapperRepo: string + upstreamRepo: string + supportSite: string + marketingSite: string + donationUrl: string | null + alerts: Alerts + dependencyMetadata: { [key: PackageId]: DependencyMetadata } + osVersion: string + hardwareRequirements: HardwareRequirements + sourceVersion: string | null + s9pk: RegistryAsset +} diff --git a/sdk/base/lib/osBindings/PasswordType.ts b/sdk/base/lib/osBindings/PasswordType.ts new file mode 100644 index 000000000..7fdcc0f5d --- /dev/null +++ b/sdk/base/lib/osBindings/PasswordType.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" + +export type PasswordType = EncryptedWire | string diff --git a/sdk/base/lib/osBindings/PathOrUrl.ts b/sdk/base/lib/osBindings/PathOrUrl.ts new file mode 100644 index 000000000..9c4ff1e28 --- /dev/null +++ b/sdk/base/lib/osBindings/PathOrUrl.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PathOrUrl = string diff --git a/sdk/base/lib/osBindings/ProcedureId.ts b/sdk/base/lib/osBindings/ProcedureId.ts new file mode 100644 index 000000000..4d9a0debd --- /dev/null +++ b/sdk/base/lib/osBindings/ProcedureId.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" + +export type ProcedureId = { procedureId: Guid } diff --git a/sdk/base/lib/osBindings/Progress.ts b/sdk/base/lib/osBindings/Progress.ts new file mode 100644 index 000000000..9509ed7e1 --- /dev/null +++ b/sdk/base/lib/osBindings/Progress.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Progress = null | boolean | { done: number; total: number | null } diff --git a/sdk/base/lib/osBindings/Public.ts b/sdk/base/lib/osBindings/Public.ts new file mode 100644 index 000000000..4fb186607 --- /dev/null +++ b/sdk/base/lib/osBindings/Public.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllPackageData } from "./AllPackageData" +import type { ServerInfo } from "./ServerInfo" + +export type Public = { + serverInfo: ServerInfo + packageData: AllPackageData + ui: unknown +} diff --git a/sdk/base/lib/osBindings/RecoverySource.ts b/sdk/base/lib/osBindings/RecoverySource.ts new file mode 100644 index 000000000..40061f215 --- /dev/null +++ b/sdk/base/lib/osBindings/RecoverySource.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BackupTargetFS } from "./BackupTargetFS" + +export type RecoverySource = + | { type: "migrate"; guid: string } + | { + type: "backup" + target: BackupTargetFS + password: Password + serverId: string + } diff --git a/sdk/base/lib/osBindings/RegistryAsset.ts b/sdk/base/lib/osBindings/RegistryAsset.ts new file mode 100644 index 000000000..41f09431f --- /dev/null +++ b/sdk/base/lib/osBindings/RegistryAsset.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnySignature } from "./AnySignature" +import type { AnyVerifyingKey } from "./AnyVerifyingKey" + +export type RegistryAsset = { + publishedAt: string + url: string + commitment: Commitment + signatures: { [key: AnyVerifyingKey]: AnySignature } +} diff --git a/sdk/base/lib/osBindings/RegistryInfo.ts b/sdk/base/lib/osBindings/RegistryInfo.ts new file mode 100644 index 000000000..f9265fdec --- /dev/null +++ b/sdk/base/lib/osBindings/RegistryInfo.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Category } from "./Category" +import type { DataUrl } from "./DataUrl" + +export type RegistryInfo = { + name: string | null + icon: DataUrl | null + categories: { [key: string]: Category } +} diff --git a/sdk/base/lib/osBindings/RemoveCategoryParams.ts b/sdk/base/lib/osBindings/RemoveCategoryParams.ts new file mode 100644 index 000000000..4f468fee7 --- /dev/null +++ b/sdk/base/lib/osBindings/RemoveCategoryParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoveCategoryParams = { id: string } diff --git a/sdk/base/lib/osBindings/RemoveVersionParams.ts b/sdk/base/lib/osBindings/RemoveVersionParams.ts new file mode 100644 index 000000000..2c974de56 --- /dev/null +++ b/sdk/base/lib/osBindings/RemoveVersionParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoveVersionParams = { version: string } diff --git a/sdk/base/lib/osBindings/ReplayId.ts b/sdk/base/lib/osBindings/ReplayId.ts new file mode 100644 index 000000000..048ef183a --- /dev/null +++ b/sdk/base/lib/osBindings/ReplayId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReplayId = string diff --git a/sdk/base/lib/osBindings/RequestActionParams.ts b/sdk/base/lib/osBindings/RequestActionParams.ts new file mode 100644 index 000000000..ccc8d0e61 --- /dev/null +++ b/sdk/base/lib/osBindings/RequestActionParams.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { ActionRequestInput } from "./ActionRequestInput" +import type { ActionRequestTrigger } from "./ActionRequestTrigger" +import type { ActionSeverity } from "./ActionSeverity" +import type { PackageId } from "./PackageId" +import type { ReplayId } from "./ReplayId" + +export type RequestActionParams = { + replayId: ReplayId + packageId: PackageId + actionId: ActionId + severity: ActionSeverity + reason?: string + when?: ActionRequestTrigger + input?: ActionRequestInput +} diff --git a/sdk/base/lib/osBindings/RequestCommitment.ts b/sdk/base/lib/osBindings/RequestCommitment.ts new file mode 100644 index 000000000..89df04e4a --- /dev/null +++ b/sdk/base/lib/osBindings/RequestCommitment.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Base64 } from "./Base64" + +export type RequestCommitment = { + timestamp: number + nonce: number + size: number + blake3: Base64 +} diff --git a/sdk/base/lib/osBindings/RunActionParams.ts b/sdk/base/lib/osBindings/RunActionParams.ts new file mode 100644 index 000000000..33864d1e6 --- /dev/null +++ b/sdk/base/lib/osBindings/RunActionParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { PackageId } from "./PackageId" + +export type RunActionParams = { + packageId?: PackageId + actionId: ActionId + input: any +} diff --git a/sdk/base/lib/osBindings/Security.ts b/sdk/base/lib/osBindings/Security.ts new file mode 100644 index 000000000..333783cd2 --- /dev/null +++ b/sdk/base/lib/osBindings/Security.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Security = { ssl: boolean } diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts new file mode 100644 index 000000000..5779fa7d5 --- /dev/null +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -0,0 +1,36 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AcmeProvider } from "./AcmeProvider" +import type { AcmeSettings } from "./AcmeSettings" +import type { Governor } from "./Governor" +import type { Host } from "./Host" +import type { LshwDevice } from "./LshwDevice" +import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" +import type { ServerStatus } from "./ServerStatus" +import type { SmtpValue } from "./SmtpValue" +import type { WifiInfo } from "./WifiInfo" + +export type ServerInfo = { + arch: string + platform: string + id: string + hostname: string + host: Host + version: string + packageVersionCompat: string + postInitMigrationTodos: string[] + lastBackup: string | null + networkInterfaces: { [key: string]: NetworkInterfaceInfo } + acme: { [key: AcmeProvider]: AcmeSettings } + statusInfo: ServerStatus + wifi: WifiInfo + unreadNotificationCount: number + passwordHash: string + pubkey: string + caFingerprint: string + ntpSynced: boolean + zram: boolean + governor: Governor | null + smtp: SmtpValue | null + ram: number + devices: Array +} diff --git a/sdk/base/lib/osBindings/ServerSpecs.ts b/sdk/base/lib/osBindings/ServerSpecs.ts new file mode 100644 index 000000000..c736ca5c8 --- /dev/null +++ b/sdk/base/lib/osBindings/ServerSpecs.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServerSpecs = { cpu: string; disk: string; memory: string } diff --git a/sdk/base/lib/osBindings/ServerStatus.ts b/sdk/base/lib/osBindings/ServerStatus.ts new file mode 100644 index 000000000..6bce57333 --- /dev/null +++ b/sdk/base/lib/osBindings/ServerStatus.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BackupProgress } from "./BackupProgress" +import type { FullProgress } from "./FullProgress" +import type { PackageId } from "./PackageId" + +export type ServerStatus = { + backupProgress: { [key: PackageId]: BackupProgress } | null + updated: boolean + updateProgress: FullProgress | null + shuttingDown: boolean + restarting: boolean +} diff --git a/sdk/base/lib/osBindings/ServiceInterface.ts b/sdk/base/lib/osBindings/ServiceInterface.ts new file mode 100644 index 000000000..6a58675a4 --- /dev/null +++ b/sdk/base/lib/osBindings/ServiceInterface.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { ServiceInterfaceType } from "./ServiceInterfaceType" + +export type ServiceInterface = { + id: ServiceInterfaceId + name: string + description: string + masked: boolean + addressInfo: AddressInfo + type: ServiceInterfaceType +} diff --git a/sdk/base/lib/osBindings/ServiceInterfaceId.ts b/sdk/base/lib/osBindings/ServiceInterfaceId.ts new file mode 100644 index 000000000..3ad454e04 --- /dev/null +++ b/sdk/base/lib/osBindings/ServiceInterfaceId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServiceInterfaceId = string diff --git a/sdk/base/lib/osBindings/ServiceInterfaceType.ts b/sdk/base/lib/osBindings/ServiceInterfaceType.ts new file mode 100644 index 000000000..109691474 --- /dev/null +++ b/sdk/base/lib/osBindings/ServiceInterfaceType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServiceInterfaceType = "ui" | "p2p" | "api" diff --git a/sdk/base/lib/osBindings/Session.ts b/sdk/base/lib/osBindings/Session.ts new file mode 100644 index 000000000..f9cffaf36 --- /dev/null +++ b/sdk/base/lib/osBindings/Session.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Session = { loggedIn: string; lastActive: string; metadata: any } diff --git a/sdk/base/lib/osBindings/SessionList.ts b/sdk/base/lib/osBindings/SessionList.ts new file mode 100644 index 000000000..af36aaa8a --- /dev/null +++ b/sdk/base/lib/osBindings/SessionList.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Sessions } from "./Sessions" + +export type SessionList = { current: string | null; sessions: Sessions } diff --git a/sdk/base/lib/osBindings/Sessions.ts b/sdk/base/lib/osBindings/Sessions.ts new file mode 100644 index 000000000..bb153ddb4 --- /dev/null +++ b/sdk/base/lib/osBindings/Sessions.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Sessions = { + [key: string]: { loggedIn: string; lastActive: string; metadata: any } +} diff --git a/sdk/base/lib/osBindings/SetDataVersionParams.ts b/sdk/base/lib/osBindings/SetDataVersionParams.ts new file mode 100644 index 000000000..3b577d2b1 --- /dev/null +++ b/sdk/base/lib/osBindings/SetDataVersionParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetDataVersionParams = { version: string } diff --git a/sdk/base/lib/osBindings/SetDependenciesParams.ts b/sdk/base/lib/osBindings/SetDependenciesParams.ts new file mode 100644 index 000000000..7b34b50c9 --- /dev/null +++ b/sdk/base/lib/osBindings/SetDependenciesParams.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DependencyRequirement } from "./DependencyRequirement" + +export type SetDependenciesParams = { + dependencies: Array +} diff --git a/sdk/base/lib/osBindings/SetHealth.ts b/sdk/base/lib/osBindings/SetHealth.ts new file mode 100644 index 000000000..47f67886a --- /dev/null +++ b/sdk/base/lib/osBindings/SetHealth.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" + +export type SetHealth = { id: HealthCheckId; name: string } & ( + | { result: "success"; message: string | null } + | { result: "disabled"; message: string | null } + | { result: "starting"; message: string | null } + | { result: "loading"; message: string } + | { result: "failure"; message: string } +) diff --git a/sdk/base/lib/osBindings/SetIconParams.ts b/sdk/base/lib/osBindings/SetIconParams.ts new file mode 100644 index 000000000..f10eaa50b --- /dev/null +++ b/sdk/base/lib/osBindings/SetIconParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" + +export type SetIconParams = { icon: DataUrl } diff --git a/sdk/base/lib/osBindings/SetMainStatus.ts b/sdk/base/lib/osBindings/SetMainStatus.ts new file mode 100644 index 000000000..6dcca73e9 --- /dev/null +++ b/sdk/base/lib/osBindings/SetMainStatus.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SetMainStatusStatus } from "./SetMainStatusStatus" + +export type SetMainStatus = { status: SetMainStatusStatus } diff --git a/sdk/base/lib/osBindings/SetMainStatusStatus.ts b/sdk/base/lib/osBindings/SetMainStatusStatus.ts new file mode 100644 index 000000000..03bb4a119 --- /dev/null +++ b/sdk/base/lib/osBindings/SetMainStatusStatus.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetMainStatusStatus = "running" | "stopped" diff --git a/sdk/base/lib/osBindings/SetNameParams.ts b/sdk/base/lib/osBindings/SetNameParams.ts new file mode 100644 index 000000000..df49d7be5 --- /dev/null +++ b/sdk/base/lib/osBindings/SetNameParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetNameParams = { name: string } diff --git a/sdk/base/lib/osBindings/SetStoreParams.ts b/sdk/base/lib/osBindings/SetStoreParams.ts new file mode 100644 index 000000000..ecdd7b042 --- /dev/null +++ b/sdk/base/lib/osBindings/SetStoreParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetStoreParams = { value: any; path: string } diff --git a/sdk/base/lib/osBindings/SetupExecuteParams.ts b/sdk/base/lib/osBindings/SetupExecuteParams.ts new file mode 100644 index 000000000..17c35c346 --- /dev/null +++ b/sdk/base/lib/osBindings/SetupExecuteParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" +import type { RecoverySource } from "./RecoverySource" + +export type SetupExecuteParams = { + startOsLogicalname: string + startOsPassword: EncryptedWire + recoverySource: RecoverySource | null +} diff --git a/sdk/base/lib/osBindings/SetupProgress.ts b/sdk/base/lib/osBindings/SetupProgress.ts new file mode 100644 index 000000000..845636da3 --- /dev/null +++ b/sdk/base/lib/osBindings/SetupProgress.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FullProgress } from "./FullProgress" +import type { Guid } from "./Guid" + +export type SetupProgress = { progress: FullProgress; guid: Guid } diff --git a/sdk/base/lib/osBindings/SetupResult.ts b/sdk/base/lib/osBindings/SetupResult.ts new file mode 100644 index 000000000..3147187c1 --- /dev/null +++ b/sdk/base/lib/osBindings/SetupResult.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetupResult = { + torAddresses: Array + hostname: string + lanAddress: string + rootCa: string +} diff --git a/sdk/base/lib/osBindings/SetupStatusRes.ts b/sdk/base/lib/osBindings/SetupStatusRes.ts new file mode 100644 index 000000000..93d10c59b --- /dev/null +++ b/sdk/base/lib/osBindings/SetupStatusRes.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SetupProgress } from "./SetupProgress" +import type { SetupResult } from "./SetupResult" + +export type SetupStatusRes = + | ({ status: "complete" } & SetupResult) + | ({ status: "running" } & SetupProgress) diff --git a/sdk/base/lib/osBindings/SignAssetParams.ts b/sdk/base/lib/osBindings/SignAssetParams.ts new file mode 100644 index 000000000..39f54ad69 --- /dev/null +++ b/sdk/base/lib/osBindings/SignAssetParams.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnySignature } from "./AnySignature" + +export type SignAssetParams = { + version: string + platform: string + signature: AnySignature +} diff --git a/sdk/base/lib/osBindings/SignerInfo.ts b/sdk/base/lib/osBindings/SignerInfo.ts new file mode 100644 index 000000000..7e7aa2588 --- /dev/null +++ b/sdk/base/lib/osBindings/SignerInfo.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnyVerifyingKey } from "./AnyVerifyingKey" +import type { ContactInfo } from "./ContactInfo" + +export type SignerInfo = { + name: string + contact: Array + keys: Array +} diff --git a/sdk/base/lib/osBindings/SmtpValue.ts b/sdk/base/lib/osBindings/SmtpValue.ts new file mode 100644 index 000000000..5291d6602 --- /dev/null +++ b/sdk/base/lib/osBindings/SmtpValue.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SmtpValue = { + server: string + port: number + from: string + login: string + password: string | null +} diff --git a/sdk/base/lib/osBindings/StartStop.ts b/sdk/base/lib/osBindings/StartStop.ts new file mode 100644 index 000000000..c8be35fb7 --- /dev/null +++ b/sdk/base/lib/osBindings/StartStop.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type StartStop = "start" | "stop" diff --git a/sdk/base/lib/osBindings/TestSmtpParams.ts b/sdk/base/lib/osBindings/TestSmtpParams.ts new file mode 100644 index 000000000..06b218a34 --- /dev/null +++ b/sdk/base/lib/osBindings/TestSmtpParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TestSmtpParams = { + server: string + port: number + from: string + to: string + login: string + password: string | null +} diff --git a/sdk/base/lib/osBindings/UnsetPublicParams.ts b/sdk/base/lib/osBindings/UnsetPublicParams.ts new file mode 100644 index 000000000..db8f730e1 --- /dev/null +++ b/sdk/base/lib/osBindings/UnsetPublicParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UnsetPublicParams = { interface: string } diff --git a/sdk/base/lib/osBindings/UpdatingState.ts b/sdk/base/lib/osBindings/UpdatingState.ts new file mode 100644 index 000000000..117124f91 --- /dev/null +++ b/sdk/base/lib/osBindings/UpdatingState.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstallingInfo } from "./InstallingInfo" +import type { Manifest } from "./Manifest" + +export type UpdatingState = { + manifest: Manifest + installingInfo: InstallingInfo +} diff --git a/sdk/base/lib/osBindings/VerifyCifsParams.ts b/sdk/base/lib/osBindings/VerifyCifsParams.ts new file mode 100644 index 000000000..407e6caaa --- /dev/null +++ b/sdk/base/lib/osBindings/VerifyCifsParams.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" + +export type VerifyCifsParams = { + hostname: string + path: string + username: string + password: EncryptedWire | null +} diff --git a/sdk/base/lib/osBindings/Version.ts b/sdk/base/lib/osBindings/Version.ts new file mode 100644 index 000000000..b49b6e887 --- /dev/null +++ b/sdk/base/lib/osBindings/Version.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Version = string diff --git a/sdk/base/lib/osBindings/VersionSignerParams.ts b/sdk/base/lib/osBindings/VersionSignerParams.ts new file mode 100644 index 000000000..781e2a4df --- /dev/null +++ b/sdk/base/lib/osBindings/VersionSignerParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" + +export type VersionSignerParams = { version: string; signer: Guid } diff --git a/sdk/base/lib/osBindings/VolumeId.ts b/sdk/base/lib/osBindings/VolumeId.ts new file mode 100644 index 000000000..b4657af4c --- /dev/null +++ b/sdk/base/lib/osBindings/VolumeId.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type VolumeId = string diff --git a/sdk/base/lib/osBindings/WifiInfo.ts b/sdk/base/lib/osBindings/WifiInfo.ts new file mode 100644 index 000000000..c5103e7e2 --- /dev/null +++ b/sdk/base/lib/osBindings/WifiInfo.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WifiInfo = { + interface: string | null + ssids: Array + selected: string | null + lastRegion: string | null +} diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts new file mode 100644 index 000000000..e2ab33033 --- /dev/null +++ b/sdk/base/lib/osBindings/index.ts @@ -0,0 +1,197 @@ +export { AcceptSigners } from "./AcceptSigners" +export { AcmeProvider } from "./AcmeProvider" +export { AcmeSettings } from "./AcmeSettings" +export { ActionId } from "./ActionId" +export { ActionInput } from "./ActionInput" +export { ActionMetadata } from "./ActionMetadata" +export { ActionRequestCondition } from "./ActionRequestCondition" +export { ActionRequestEntry } from "./ActionRequestEntry" +export { ActionRequestInput } from "./ActionRequestInput" +export { ActionRequestTrigger } from "./ActionRequestTrigger" +export { ActionRequest } from "./ActionRequest" +export { ActionResultMember } from "./ActionResultMember" +export { ActionResult } from "./ActionResult" +export { ActionResultV0 } from "./ActionResultV0" +export { ActionResultV1 } from "./ActionResultV1" +export { ActionResultValue } from "./ActionResultValue" +export { ActionSeverity } from "./ActionSeverity" +export { ActionVisibility } from "./ActionVisibility" +export { AddAdminParams } from "./AddAdminParams" +export { AddAssetParams } from "./AddAssetParams" +export { AddCategoryParams } from "./AddCategoryParams" +export { AddPackageParams } from "./AddPackageParams" +export { AddressInfo } from "./AddressInfo" +export { AddSslOptions } from "./AddSslOptions" +export { AddVersionParams } from "./AddVersionParams" +export { Alerts } from "./Alerts" +export { Algorithm } from "./Algorithm" +export { AllowedStatuses } from "./AllowedStatuses" +export { AllPackageData } from "./AllPackageData" +export { AlpnInfo } from "./AlpnInfo" +export { AnySignature } from "./AnySignature" +export { AnySigningKey } from "./AnySigningKey" +export { AnyVerifyingKey } from "./AnyVerifyingKey" +export { ApiState } from "./ApiState" +export { AttachParams } from "./AttachParams" +export { BackupProgress } from "./BackupProgress" +export { BackupTargetFS } from "./BackupTargetFS" +export { Base64 } from "./Base64" +export { BindId } from "./BindId" +export { BindInfo } from "./BindInfo" +export { BindingSetPublicParams } from "./BindingSetPublicParams" +export { BindOptions } from "./BindOptions" +export { BindParams } from "./BindParams" +export { Blake3Commitment } from "./Blake3Commitment" +export { BlockDev } from "./BlockDev" +export { BuildArg } from "./BuildArg" +export { CallbackId } from "./CallbackId" +export { Category } from "./Category" +export { CheckDependenciesParam } from "./CheckDependenciesParam" +export { CheckDependenciesResult } from "./CheckDependenciesResult" +export { Cifs } from "./Cifs" +export { ClearActionRequestsParams } from "./ClearActionRequestsParams" +export { ClearActionsParams } from "./ClearActionsParams" +export { ClearBindingsParams } from "./ClearBindingsParams" +export { ClearCallbacksParams } from "./ClearCallbacksParams" +export { ClearServiceInterfacesParams } from "./ClearServiceInterfacesParams" +export { CliSetIconParams } from "./CliSetIconParams" +export { ContactInfo } from "./ContactInfo" +export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams" +export { CurrentDependencies } from "./CurrentDependencies" +export { CurrentDependencyInfo } from "./CurrentDependencyInfo" +export { DataUrl } from "./DataUrl" +export { Dependencies } from "./Dependencies" +export { DependencyKind } from "./DependencyKind" +export { DependencyMetadata } from "./DependencyMetadata" +export { DependencyRequirement } from "./DependencyRequirement" +export { DepInfo } from "./DepInfo" +export { Description } from "./Description" +export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams" +export { DeviceFilter } from "./DeviceFilter" +export { DomainConfig } from "./DomainConfig" +export { Duration } from "./Duration" +export { EchoParams } from "./EchoParams" +export { EditSignerParams } from "./EditSignerParams" +export { EncryptedWire } from "./EncryptedWire" +export { ExportActionParams } from "./ExportActionParams" +export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" +export { ExposeForDependentsParams } from "./ExposeForDependentsParams" +export { ForgetInterfaceParams } from "./ForgetInterfaceParams" +export { FullIndex } from "./FullIndex" +export { FullProgress } from "./FullProgress" +export { GetActionInputParams } from "./GetActionInputParams" +export { GetHostInfoParams } from "./GetHostInfoParams" +export { GetOsAssetParams } from "./GetOsAssetParams" +export { GetOsVersionParams } from "./GetOsVersionParams" +export { GetPackageParams } from "./GetPackageParams" +export { GetPackageResponseFull } from "./GetPackageResponseFull" +export { GetPackageResponse } from "./GetPackageResponse" +export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams" +export { GetServicePortForwardParams } from "./GetServicePortForwardParams" +export { GetSslCertificateParams } from "./GetSslCertificateParams" +export { GetSslKeyParams } from "./GetSslKeyParams" +export { GetStatusParams } from "./GetStatusParams" +export { GetStoreParams } from "./GetStoreParams" +export { GetSystemSmtpParams } from "./GetSystemSmtpParams" +export { GitHash } from "./GitHash" +export { Governor } from "./Governor" +export { Guid } from "./Guid" +export { HardwareRequirements } from "./HardwareRequirements" +export { HealthCheckId } from "./HealthCheckId" +export { HostAddress } from "./HostAddress" +export { HostId } from "./HostId" +export { HostnameInfo } from "./HostnameInfo" +export { Hosts } from "./Hosts" +export { Host } from "./Host" +export { ImageConfig } from "./ImageConfig" +export { ImageId } from "./ImageId" +export { ImageMetadata } from "./ImageMetadata" +export { ImageSource } from "./ImageSource" +export { InitProgressRes } from "./InitProgressRes" +export { InstalledState } from "./InstalledState" +export { InstalledVersionParams } from "./InstalledVersionParams" +export { InstallingInfo } from "./InstallingInfo" +export { InstallingState } from "./InstallingState" +export { InstallParams } from "./InstallParams" +export { IpHostname } from "./IpHostname" +export { IpInfo } from "./IpInfo" +export { ListPackageSignersParams } from "./ListPackageSignersParams" +export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" +export { ListVersionSignersParams } from "./ListVersionSignersParams" +export { LoginParams } from "./LoginParams" +export { LshwDevice } from "./LshwDevice" +export { LshwDisplay } from "./LshwDisplay" +export { LshwProcessor } from "./LshwProcessor" +export { MainStatus } from "./MainStatus" +export { Manifest } from "./Manifest" +export { MaybeUtf8String } from "./MaybeUtf8String" +export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +export { MountParams } from "./MountParams" +export { MountTarget } from "./MountTarget" +export { NamedHealthCheckResult } from "./NamedHealthCheckResult" +export { NamedProgress } from "./NamedProgress" +export { NetInfo } from "./NetInfo" +export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" +export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicParams" +export { NetworkInterfaceType } from "./NetworkInterfaceType" +export { OnionHostname } from "./OnionHostname" +export { OsIndex } from "./OsIndex" +export { OsVersionInfoMap } from "./OsVersionInfoMap" +export { OsVersionInfo } from "./OsVersionInfo" +export { PackageDataEntry } from "./PackageDataEntry" +export { PackageDetailLevel } from "./PackageDetailLevel" +export { PackageId } from "./PackageId" +export { PackageIndex } from "./PackageIndex" +export { PackageInfoShort } from "./PackageInfoShort" +export { PackageInfo } from "./PackageInfo" +export { PackageSignerParams } from "./PackageSignerParams" +export { PackageState } from "./PackageState" +export { PackageVersionInfo } from "./PackageVersionInfo" +export { PasswordType } from "./PasswordType" +export { PathOrUrl } from "./PathOrUrl" +export { ProcedureId } from "./ProcedureId" +export { Progress } from "./Progress" +export { Public } from "./Public" +export { RecoverySource } from "./RecoverySource" +export { RegistryAsset } from "./RegistryAsset" +export { RegistryInfo } from "./RegistryInfo" +export { RemoveCategoryParams } from "./RemoveCategoryParams" +export { RemoveVersionParams } from "./RemoveVersionParams" +export { ReplayId } from "./ReplayId" +export { RequestActionParams } from "./RequestActionParams" +export { RequestCommitment } from "./RequestCommitment" +export { RunActionParams } from "./RunActionParams" +export { Security } from "./Security" +export { ServerInfo } from "./ServerInfo" +export { ServerSpecs } from "./ServerSpecs" +export { ServerStatus } from "./ServerStatus" +export { ServiceInterfaceId } from "./ServiceInterfaceId" +export { ServiceInterface } from "./ServiceInterface" +export { ServiceInterfaceType } from "./ServiceInterfaceType" +export { SessionList } from "./SessionList" +export { Sessions } from "./Sessions" +export { Session } from "./Session" +export { SetDataVersionParams } from "./SetDataVersionParams" +export { SetDependenciesParams } from "./SetDependenciesParams" +export { SetHealth } from "./SetHealth" +export { SetIconParams } from "./SetIconParams" +export { SetMainStatusStatus } from "./SetMainStatusStatus" +export { SetMainStatus } from "./SetMainStatus" +export { SetNameParams } from "./SetNameParams" +export { SetStoreParams } from "./SetStoreParams" +export { SetupExecuteParams } from "./SetupExecuteParams" +export { SetupProgress } from "./SetupProgress" +export { SetupResult } from "./SetupResult" +export { SetupStatusRes } from "./SetupStatusRes" +export { SignAssetParams } from "./SignAssetParams" +export { SignerInfo } from "./SignerInfo" +export { SmtpValue } from "./SmtpValue" +export { StartStop } from "./StartStop" +export { TestSmtpParams } from "./TestSmtpParams" +export { UnsetPublicParams } from "./UnsetPublicParams" +export { UpdatingState } from "./UpdatingState" +export { VerifyCifsParams } from "./VerifyCifsParams" +export { VersionSignerParams } from "./VersionSignerParams" +export { Version } from "./Version" +export { VolumeId } from "./VolumeId" +export { WifiInfo } from "./WifiInfo" diff --git a/sdk/base/lib/s9pk/index.ts b/sdk/base/lib/s9pk/index.ts new file mode 100644 index 000000000..84a1ec644 --- /dev/null +++ b/sdk/base/lib/s9pk/index.ts @@ -0,0 +1,68 @@ +import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings" +import { ArrayBufferReader, MerkleArchive } from "./merkleArchive" +import mime from "mime-types" + +const magicAndVersion = new Uint8Array([59, 59, 2]) + +export function compare(a: Uint8Array, b: Uint8Array) { + if (a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +export class S9pk { + private constructor( + readonly manifest: Manifest, + readonly archive: MerkleArchive, + readonly size: number, + ) {} + static async deserialize( + source: Blob, + commitment: MerkleArchiveCommitment | null, + ): Promise { + const header = new ArrayBufferReader( + await source + .slice(0, magicAndVersion.length + MerkleArchive.headerSize) + .arrayBuffer(), + ) + const magicVersion = new Uint8Array(header.next(magicAndVersion.length)) + if (!compare(magicVersion, magicAndVersion)) { + throw new Error("Invalid Magic or Unexpected Version") + } + + const archive = await MerkleArchive.deserialize( + source, + "s9pk", + header, + commitment, + ) + + const manifest = JSON.parse( + new TextDecoder().decode( + await archive.contents + .getPath(["manifest.json"]) + ?.verifiedFileContents(), + ), + ) + + return new S9pk(manifest, archive, source.length) + } + async icon(): Promise { + const iconName = Object.keys(this.archive.contents.contents).find( + (name) => + name.startsWith("icon.") && + (mime.contentType(name) || null)?.startsWith("image/"), + ) + if (!iconName) { + throw new Error("no icon found in archive") + } + return ( + `data:${mime.contentType(iconName)};base64,` + + Buffer.from( + await this.archive.contents.getPath([iconName])!.verifiedFileContents(), + ).toString("base64") + ) + } +} diff --git a/sdk/base/lib/s9pk/merkleArchive/directoryContents.ts b/sdk/base/lib/s9pk/merkleArchive/directoryContents.ts new file mode 100644 index 000000000..dab7ef53a --- /dev/null +++ b/sdk/base/lib/s9pk/merkleArchive/directoryContents.ts @@ -0,0 +1,80 @@ +import { ArrayBufferReader, Entry } from "." +import { blake3 } from "@noble/hashes/blake3" +import { serializeVarint } from "./varint" +import { FileContents } from "./fileContents" +import { compare } from ".." + +export class DirectoryContents { + static readonly headerSize = + 8 + // position: u64 BE + 8 // size: u64 BE + private constructor(readonly contents: { [name: string]: Entry }) {} + static async deserialize( + source: Blob, + header: ArrayBufferReader, + sighash: Uint8Array, + maxSize: bigint, + ): Promise { + const position = header.nextU64() + const size = header.nextU64() + if (size > maxSize) { + throw new Error("size is greater than signed") + } + + const tocReader = new ArrayBufferReader( + await source + .slice(Number(position), Number(position + size)) + .arrayBuffer(), + ) + const len = tocReader.nextVarint() + const entries: { [name: string]: Entry } = {} + for (let i = 0; i < len; i++) { + const name = tocReader.nextVarstring() + const entry = await Entry.deserialize(source, tocReader) + entries[name] = entry + } + + const res = new DirectoryContents(entries) + + if (!compare(res.sighash(), sighash)) { + throw new Error("hash sum does not match") + } + + return res + } + sighash(): Uint8Array { + const hasher = blake3.create({}) + const names = Object.keys(this.contents).sort() + hasher.update(new Uint8Array(serializeVarint(names.length))) + for (const name of names) { + const entry = this.contents[name] + const nameBuf = new TextEncoder().encode(name) + hasher.update(new Uint8Array(serializeVarint(nameBuf.length))) + hasher.update(nameBuf) + hasher.update(new Uint8Array(entry.hash)) + const sizeBuf = new Uint8Array(8) + new DataView(sizeBuf.buffer).setBigUint64(0, entry.size) + hasher.update(sizeBuf) + hasher.update(new Uint8Array([0])) + } + + return hasher.digest() + } + getPath(path: string[]): Entry | null { + if (path.length === 0) { + return null + } + const next = this.contents[path[0]] + const rest = path.slice(1) + if (next === undefined) { + return null + } + if (rest.length === 0) { + return next + } + if (next.contents instanceof DirectoryContents) { + return next.contents.getPath(rest) + } + return null + } +} diff --git a/sdk/base/lib/s9pk/merkleArchive/fileContents.ts b/sdk/base/lib/s9pk/merkleArchive/fileContents.ts new file mode 100644 index 000000000..7a936f1e8 --- /dev/null +++ b/sdk/base/lib/s9pk/merkleArchive/fileContents.ts @@ -0,0 +1,24 @@ +import { blake3 } from "@noble/hashes/blake3" +import { ArrayBufferReader } from "." +import { compare } from ".." + +export class FileContents { + private constructor(readonly contents: Blob) {} + static deserialize( + source: Blob, + header: ArrayBufferReader, + size: bigint, + ): FileContents { + const position = header.nextU64() + return new FileContents( + source.slice(Number(position), Number(position + size)), + ) + } + async verified(hash: Uint8Array): Promise { + const res = await this.contents.arrayBuffer() + if (!compare(hash, blake3(new Uint8Array(res)))) { + throw new Error("hash sum mismatch") + } + return res + } +} diff --git a/sdk/base/lib/s9pk/merkleArchive/index.ts b/sdk/base/lib/s9pk/merkleArchive/index.ts new file mode 100644 index 000000000..068363599 --- /dev/null +++ b/sdk/base/lib/s9pk/merkleArchive/index.ts @@ -0,0 +1,167 @@ +import { MerkleArchiveCommitment } from "../../osBindings" +import { DirectoryContents } from "./directoryContents" +import { FileContents } from "./fileContents" +import { ed25519ph } from "@noble/curves/ed25519" +import { sha512 } from "@noble/hashes/sha2" +import { VarIntProcessor } from "./varint" +import { compare } from ".." + +const maxVarstringLen = 1024 * 1024 + +export type Signer = { + pubkey: Uint8Array + signature: Uint8Array + maxSize: bigint + context: string +} + +export class ArrayBufferReader { + constructor(private buffer: ArrayBuffer) {} + next(length: number): ArrayBuffer { + const res = this.buffer.slice(0, length) + this.buffer = this.buffer.slice(length) + return res + } + nextU64(): bigint { + return new DataView(this.next(8)).getBigUint64(0) + } + nextVarint(): number { + const p = new VarIntProcessor() + while (!p.finished()) { + p.push(new Uint8Array(this.buffer.slice(0, 1))[0]) + this.buffer = this.buffer.slice(1) + } + const res = p.decode() + if (res === null) { + throw new Error("Reached EOF") + } + return res + } + nextVarstring(): string { + const len = Math.min(this.nextVarint(), maxVarstringLen) + return new TextDecoder().decode(this.next(len)) + } +} + +export class MerkleArchive { + static readonly headerSize = + 32 + // pubkey + 64 + // signature + 32 + // sighash + 8 + // size + DirectoryContents.headerSize + private constructor( + readonly signer: Signer, + readonly contents: DirectoryContents, + ) {} + static async deserialize( + source: Blob, + context: string, + header: ArrayBufferReader, + commitment: MerkleArchiveCommitment | null, + ): Promise { + const pubkey = new Uint8Array(header.next(32)) + const signature = new Uint8Array(header.next(64)) + const sighash = new Uint8Array(header.next(32)) + const rootMaxSizeBytes = header.next(8) + const maxSize = new DataView(rootMaxSizeBytes).getBigUint64(0) + + if ( + !ed25519ph.verify( + signature, + new Uint8Array( + await new Blob([sighash, rootMaxSizeBytes]).arrayBuffer(), + ), + pubkey, + { + context: new TextEncoder().encode(context), + zip215: true, + }, + ) + ) { + throw new Error("signature verification failed") + } + + if (commitment) { + if ( + !compare( + sighash, + new Uint8Array(Buffer.from(commitment.rootSighash, "base64").buffer), + ) + ) { + throw new Error("merkle root mismatch") + } + if (maxSize > commitment.rootMaxsize) { + throw new Error("root directory max size too large") + } + } else if (maxSize > 1024 * 1024) { + throw new Error( + "root directory max size over 1MiB, cancelling download in case of DOS attack", + ) + } + + const contents = await DirectoryContents.deserialize( + source, + header, + sighash, + maxSize, + ) + + return new MerkleArchive( + { + pubkey, + signature, + maxSize, + context, + }, + contents, + ) + } +} + +export class Entry { + private constructor( + readonly hash: Uint8Array, + readonly size: bigint, + readonly contents: EntryContents, + ) {} + static async deserialize( + source: Blob, + header: ArrayBufferReader, + ): Promise { + const hash = new Uint8Array(header.next(32)) + const size = header.nextU64() + const contents = await deserializeEntryContents(source, header, hash, size) + + return new Entry(new Uint8Array(hash), size, contents) + } + async verifiedFileContents(): Promise { + if (!this.contents) { + throw new Error("file is missing from archive") + } + if (!(this.contents instanceof FileContents)) { + throw new Error("is not a regular file") + } + return this.contents.verified(this.hash) + } +} + +export type EntryContents = null | FileContents | DirectoryContents +async function deserializeEntryContents( + source: Blob, + header: ArrayBufferReader, + hash: Uint8Array, + size: bigint, +): Promise { + const typeId = new Uint8Array(header.next(1))[0] + switch (typeId) { + case 0: + return null + case 1: + return FileContents.deserialize(source, header, size) + case 2: + return DirectoryContents.deserialize(source, header, hash, size) + default: + throw new Error(`Unknown type id ${typeId} found in MerkleArchive`) + } +} diff --git a/sdk/base/lib/s9pk/merkleArchive/varint.ts b/sdk/base/lib/s9pk/merkleArchive/varint.ts new file mode 100644 index 000000000..a6a425289 --- /dev/null +++ b/sdk/base/lib/s9pk/merkleArchive/varint.ts @@ -0,0 +1,64 @@ +import { asError } from "../../util" + +const msb = 0x80 +const dropMsb = 0x7f +const maxSize = Math.floor((8 * 8 + 7) / 7) + +export class VarIntProcessor { + private buf: Uint8Array + private i: number + constructor() { + this.buf = new Uint8Array(maxSize) + this.i = 0 + } + push(b: number) { + if (this.i >= maxSize) { + throw new Error("Unterminated varint") + } + this.buf[this.i] = b + this.i += 1 + } + finished(): boolean { + return this.i > 0 && (this.buf[this.i - 1] & msb) === 0 + } + decode(): number | null { + let result = 0 + let shift = 0 + let success = false + for (let i = 0; i < this.i; i++) { + const b = this.buf[i] + const msbDropped = b & dropMsb + result |= msbDropped << shift + shift += 7 + + if ((b & msb) == 0 || shift > 9 * 7) { + success = (b & msb) === 0 + break + } + } + + if (success) { + return result + } else { + console.error(asError(this.buf)) + return null + } + } +} + +export function serializeVarint(int: number): ArrayBuffer { + const buf = new Uint8Array(maxSize) + let n = int + let i = 0 + + while (n >= msb) { + buf[i] = msb | n + i += 1 + n >>= 7 + } + + buf[i] = n + i += 1 + + return buf.slice(0, i).buffer +} diff --git a/sdk/base/lib/test/exverList.test.ts b/sdk/base/lib/test/exverList.test.ts new file mode 100644 index 000000000..e29a9f0d1 --- /dev/null +++ b/sdk/base/lib/test/exverList.test.ts @@ -0,0 +1,355 @@ +import { VersionRange, ExtendedVersion } from "../exver" +describe("ExVer", () => { + { + { + const checker = VersionRange.parse("*") + test("VersionRange.parse('*')", () => { + checker.satisfiedBy(ExtendedVersion.parse("1:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5.6")) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + test("VersionRange.parse('*') invalid", () => { + expect(() => checker.satisfiedBy(ExtendedVersion.parse("a"))).toThrow() + expect(() => checker.satisfiedBy(ExtendedVersion.parse(""))).toThrow() + expect(() => + checker.satisfiedBy(ExtendedVersion.parse("1..3")), + ).toThrow() + }) + } + + { + const checker = VersionRange.parse(">1.2.3:4") + test(`VersionRange.parse(">1.2.3:4") valid`, () => { + expect( + checker.satisfiedBy(ExtendedVersion.parse("2-beta.123:0")), + ).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("=1.2.3") + test(`VersionRange.parse("=1.2.3") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse("=1.2.3") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse(">=1.2.3:4") + test(`VersionRange.parse(">=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("<1.2.3:4") + test(`VersionRange.parse("<1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + { + const checker = VersionRange.parse("<=1.2.3:4") + test(`VersionRange.parse("<=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + } + + { + const checkA = VersionRange.parse(">1") + const checkB = VersionRange.parse("<=2") + + const checker = checkA.and(checkB) + test(`simple and(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + }) + test(`simple and(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checkA = VersionRange.parse("<1") + const checkB = VersionRange.parse("=2") + + const checker = checkA.or(checkB) + test(`simple or(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("0.1:0"))).toEqual( + true, + ) + }) + test(`simple or(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + }) + } + + { + const checker = VersionRange.parse("~1.2") + test(`VersionRange.parse(~1.2) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + true, + ) + }) + test(`VersionRange.parse(~1.2) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + false, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + }) + } + + { + const checker = VersionRange.parse("~1.2").not() + test(`VersionRange.parse(~1.2).not() valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`VersionRange.parse(~1.2).not() invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!~1.2") + test(`!(VersionRange.parse(~1.2)) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`!(VersionRange.parse(~1.2)) invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!>1.2.3:4") + test(`VersionRange.parse("!>1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("!>1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + + { + test(">1 && =1.2", () => { + const checker = VersionRange.parse(">1 && =1.2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + test("=1 || =2", () => { + const checker = VersionRange.parse("=1 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test(">1 && =1.2 || =2", () => { + const checker = VersionRange.parse(">1 && =1.2 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { + const checker = VersionRange.parse("<1.5 && >1 || >1.5 && <3") + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.5:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("Compare function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compare(b)).toEqual("less") + expect(b.compare(a)).toEqual("greater") + expect(a.compare(a)).toEqual("equal") + }) + test("Compare for sort function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compareForSort(b)).toEqual(-1) + expect(b.compareForSort(a)).toEqual(1) + expect(a.compareForSort(a)).toEqual(0) + }) + } + } +}) diff --git a/sdk/base/lib/test/graph.test.ts b/sdk/base/lib/test/graph.test.ts new file mode 100644 index 000000000..a738123d6 --- /dev/null +++ b/sdk/base/lib/test/graph.test.ts @@ -0,0 +1,148 @@ +import { Graph } from "../util" + +describe("graph", () => { + { + { + test("findVertex", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [{ from: baz, metadata: "baz-qux" }], + [], + ) + const match = Array.from(graph.findVertex((v) => v.metadata === "qux")) + expect(match).toHaveLength(1) + expect(match[0]).toBe(qux) + }) + test("shortestPathA", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [{ from: baz, metadata: "baz-qux" }], + [], + ) + graph.addEdge("foo-qux", foo, qux) + expect(graph.shortestPath(foo, qux) || []).toHaveLength(1) + }) + test("shortestPathB", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [{ from: baz, metadata: "baz-qux" }], + [], + ) + graph.addEdge("bar-qux", bar, qux) + expect(graph.shortestPath(foo, qux) || []).toHaveLength(2) + }) + test("shortestPathC", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [{ from: baz, metadata: "baz-qux" }], + [{ to: foo, metadata: "qux-foo" }], + ) + expect(graph.shortestPath(foo, qux) || []).toHaveLength(3) + }) + test("bfs", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [ + { from: foo, metadata: "foo-qux" }, + { from: baz, metadata: "baz-qux" }, + ], + [], + ) + const bfs = Array.from(graph.breadthFirstSearch(foo)) + expect(bfs).toHaveLength(4) + expect(bfs[0]).toBe(foo) + expect(bfs[1]).toBe(bar) + expect(bfs[2]).toBe(qux) + expect(bfs[3]).toBe(baz) + }) + test("reverseBfs", () => { + const graph = new Graph() + const foo = graph.addVertex("foo", [], []) + const bar = graph.addVertex( + "bar", + [{ from: foo, metadata: "foo-bar" }], + [], + ) + const baz = graph.addVertex( + "baz", + [{ from: bar, metadata: "bar-baz" }], + [], + ) + const qux = graph.addVertex( + "qux", + [ + { from: foo, metadata: "foo-qux" }, + { from: baz, metadata: "baz-qux" }, + ], + [], + ) + const bfs = Array.from(graph.reverseBreadthFirstSearch(qux)) + expect(bfs).toHaveLength(4) + expect(bfs[0]).toBe(qux) + expect(bfs[1]).toBe(foo) + expect(bfs[2]).toBe(baz) + expect(bfs[3]).toBe(bar) + }) + } + } +}) diff --git a/sdk/base/lib/test/inputSpecTypes.test.ts b/sdk/base/lib/test/inputSpecTypes.test.ts new file mode 100644 index 000000000..6767faf20 --- /dev/null +++ b/sdk/base/lib/test/inputSpecTypes.test.ts @@ -0,0 +1,29 @@ +import { + ListValueSpecOf, + isValueSpecListOf, +} from "../actions/input/inputSpecTypes" +import { InputSpec } from "../actions/input/builder/inputSpec" +import { List } from "../actions/input/builder/list" +import { Value } from "../actions/input/builder/value" + +describe("InputSpec Types", () => { + test("isValueSpecListOf", async () => { + const options = [List.obj, List.text] + for (const option of options) { + const test = (option as any)( + {} as any, + { spec: InputSpec.of({}) } as any, + ) as any + const someList = await Value.list(test).build({} as any) + if (isValueSpecListOf(someList, "text")) { + someList.spec satisfies ListValueSpecOf<"text"> + } else if (isValueSpecListOf(someList, "object")) { + someList.spec satisfies ListValueSpecOf<"object"> + } else { + throw new Error( + "Failed to figure out the type: " + JSON.stringify(someList), + ) + } + } + }) +}) diff --git a/sdk/base/lib/test/startosTypeValidation.test.ts b/sdk/base/lib/test/startosTypeValidation.test.ts new file mode 100644 index 000000000..2de7b43a4 --- /dev/null +++ b/sdk/base/lib/test/startosTypeValidation.test.ts @@ -0,0 +1,93 @@ +import { Effects } from "../types" +import { + CheckDependenciesParam, + ClearActionRequestsParams, + ClearActionsParams, + ClearBindingsParams, + ClearCallbacksParams, + ClearServiceInterfacesParams, + GetActionInputParams, + GetStatusParams, + RequestActionParams, + RunActionParams, + SetDataVersionParams, + SetMainStatus, +} from ".././osBindings" +import { CreateSubcontainerFsParams } from ".././osBindings" +import { DestroySubcontainerFsParams } from ".././osBindings" +import { BindParams } from ".././osBindings" +import { GetHostInfoParams } from ".././osBindings" +import { SetHealth } from ".././osBindings" +import { ExposeForDependentsParams } from ".././osBindings" +import { GetSslCertificateParams } from ".././osBindings" +import { GetSslKeyParams } from ".././osBindings" +import { GetServiceInterfaceParams } from ".././osBindings" +import { SetDependenciesParams } from ".././osBindings" +import { GetSystemSmtpParams } from ".././osBindings" +import { GetServicePortForwardParams } from ".././osBindings" +import { ExportServiceInterfaceParams } from ".././osBindings" +import { ListServiceInterfacesParams } from ".././osBindings" +import { ExportActionParams } from ".././osBindings" +import { MountParams } from ".././osBindings" +import { StringObject } from "../util" +function typeEquality(_a: ExpectedType) {} + +type WithCallback = Omit & { callback: () => void } + +type EffectsTypeChecker = { + [K in keyof T]: T[K] extends (args: infer A) => any + ? A + : T[K] extends StringObject + ? EffectsTypeChecker + : never +} + +describe("startosTypeValidation ", () => { + test(`checking the params match`, () => { + typeEquality({ + constRetry: {}, + clearCallbacks: {} as ClearCallbacksParams, + action: { + clear: {} as ClearActionsParams, + export: {} as ExportActionParams, + getInput: {} as GetActionInputParams, + run: {} as RunActionParams, + request: {} as RequestActionParams, + clearRequests: {} as ClearActionRequestsParams, + }, + subcontainer: { + createFs: {} as CreateSubcontainerFsParams, + destroyFs: {} as DestroySubcontainerFsParams, + }, + clearBindings: {} as ClearBindingsParams, + getInstalledPackages: undefined, + bind: {} as BindParams, + getHostInfo: {} as WithCallback, + restart: undefined, + shutdown: undefined, + setDataVersion: {} as SetDataVersionParams, + getDataVersion: undefined, + setHealth: {} as SetHealth, + exposeForDependents: {} as ExposeForDependentsParams, + getSslCertificate: {} as WithCallback, + getSslKey: {} as GetSslKeyParams, + getServiceInterface: {} as WithCallback, + setDependencies: {} as SetDependenciesParams, + store: { + get: {} as any, // as GetStoreParams, + set: {} as any, // as SetStoreParams, + }, + getSystemSmtp: {} as WithCallback, + getContainerIp: undefined, + getServicePortForward: {} as GetServicePortForwardParams, + clearServiceInterfaces: {} as ClearServiceInterfacesParams, + exportServiceInterface: {} as ExportServiceInterfaceParams, + listServiceInterfaces: {} as WithCallback, + mount: {} as MountParams, + checkDependencies: {} as CheckDependenciesParam, + getDependencies: undefined, + getStatus: {} as WithCallback, + setMainStatus: {} as SetMainStatus, + }) + }) +}) diff --git a/sdk/base/lib/test/util.deepMerge.test.ts b/sdk/base/lib/test/util.deepMerge.test.ts new file mode 100644 index 000000000..74ff92c26 --- /dev/null +++ b/sdk/base/lib/test/util.deepMerge.test.ts @@ -0,0 +1,30 @@ +import { deepEqual } from "../util" +import { deepMerge } from "../util" + +describe("deepMerge", () => { + test("deepMerge({}, {a: 1}, {b: 2}) should return {a: 1, b: 2}", () => { + expect(deepMerge({}, { a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }) + }) + test("deepMerge(null, [1,2,3]) should equal [1,2,3]", () => { + expect(deepMerge(null, [1, 2, 3])).toEqual([1, 2, 3]) + }) + test("deepMerge({a: {b: 1, c:2}}, {a: {b: 3}}) should equal {a: {b: 3, c: 2}}", () => { + expect(deepMerge({ a: { b: 1, c: 2 } }, { a: { b: 3 } })).toEqual({ + a: { b: 3, c: 2 }, + }) + }) + test("deepMerge({a: {b: 1, c:2}}, {a: {b: 3}}) should equal {a: {b: 3, c: 2}} with deep equal", () => { + expect( + deepEqual(deepMerge({ a: { b: 1, c: 2 } }, { a: { b: 3 } }), { + a: { b: 3, c: 2 }, + }), + ).toBeTruthy() + }) + test("Test that merging lists has Set semantics", () => { + const merge = deepMerge(["a", "b"], ["b", "c"]) + expect(merge).toHaveLength(3) + expect(merge).toContain("a") + expect(merge).toContain("b") + expect(merge).toContain("c") + }) +}) diff --git a/sdk/base/lib/test/util.getNetworkInterface.test.ts b/sdk/base/lib/test/util.getNetworkInterface.test.ts new file mode 100644 index 000000000..df7ac73c6 --- /dev/null +++ b/sdk/base/lib/test/util.getNetworkInterface.test.ts @@ -0,0 +1,20 @@ +import { getHostname } from "../util/getServiceInterface" + +describe("getHostname ", () => { + const inputToExpected = [ + ["http://localhost:3000", "localhost"], + ["http://localhost", "localhost"], + ["localhost", "localhost"], + ["http://127.0.0.1/", "127.0.0.1"], + ["http://127.0.0.1/testing/1234?314345", "127.0.0.1"], + ["127.0.0.1/", "127.0.0.1"], + ["http://mail.google.com/", "mail.google.com"], + ["mail.google.com/", "mail.google.com"], + ] + + for (const [input, expectValue] of inputToExpected) { + test(`should return ${expectValue} for ${input}`, () => { + expect(getHostname(input)).toEqual(expectValue) + }) + } +}) diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts new file mode 100644 index 000000000..85a8c4404 --- /dev/null +++ b/sdk/base/lib/types.ts @@ -0,0 +1,209 @@ +export * as inputSpecTypes from "./actions/input/inputSpecTypes" + +import { + DependencyRequirement, + NamedHealthCheckResult, + Manifest, + ServiceInterface, + ActionId, +} from "./osBindings" +import { Affine, StringObject, ToKebab } from "./util" +import { Action, Actions } from "./actions/setupActions" +import { Effects } from "./Effects" +export { Effects } +export * from "./osBindings" +export { SDKManifest } from "./types/ManifestTypes" +export { + RequiredDependenciesOf as RequiredDependencies, + OptionalDependenciesOf as OptionalDependencies, + CurrentDependenciesResult, +} from "./dependencies/setupDependencies" + +export type ExposedStorePaths = string[] & Affine<"ExposedStorePaths"> +declare const HealthProof: unique symbol +export type HealthReceipt = { + [HealthProof]: never +} + +export type DaemonBuildable = { + build(): Promise<{ + term(): Promise + }> +} + +export type ServiceInterfaceType = "ui" | "p2p" | "api" +export type Signals = NodeJS.Signals +export const SIGTERM: Signals = "SIGTERM" +export const SIGKILL: Signals = "SIGKILL" +export const NO_TIMEOUT = -1 + +export type PathMaker = (options: { volume: string; path: string }) => string +export type MaybePromise = Promise | A +export namespace ExpectedExports { + version: 1 + + /** For backing up service data though the startOS UI */ + export type createBackup = (options: { effects: Effects }) => Promise + /** For restoring service data that was previously backed up using the startOS UI create backup flow. Backup restores are also triggered via the startOS UI, or doing a system restore flow during setup. */ + export type restoreBackup = (options: { + effects: Effects + }) => Promise + + /** + * This is the entrypoint for the main container. Used to start up something like the service that the + * package represents, like running a bitcoind in a bitcoind-wrapper. + */ + export type main = (options: { + effects: Effects + started(onTerm: () => PromiseLike): PromiseLike + }) => Promise + + /** + * After a shutdown, if we wanted to do any operations to clean up things, like + * set the action as unavailable or something. + */ + export type afterShutdown = (options: { + effects: Effects + }) => Promise + + /** + * Every time a service launches (both on startup, and on install) this function is called before packageInit + * Can be used to register callbacks + */ + export type containerInit = (options: { + effects: Effects + }) => Promise + + /** + * Every time a package completes an install, this function is called before the main. + * Can be used to do migration like things. + */ + export type packageInit = (options: { effects: Effects }) => Promise + /** This will be ran during any time a package is uninstalled, for example during a update + * this will be called. + */ + export type packageUninit = (options: { + effects: Effects + nextVersion: null | string + }) => Promise + + export type manifest = Manifest + + export type actions = Actions< + any, + Record> + > +} +export type ABI = { + createBackup: ExpectedExports.createBackup + restoreBackup: ExpectedExports.restoreBackup + main: ExpectedExports.main + afterShutdown: ExpectedExports.afterShutdown + containerInit: ExpectedExports.containerInit + packageInit: ExpectedExports.packageInit + packageUninit: ExpectedExports.packageUninit + manifest: ExpectedExports.manifest + actions: ExpectedExports.actions +} +export type TimeMs = number +export type VersionString = string + +declare const DaemonProof: unique symbol +export type DaemonReceipt = { + [DaemonProof]: never +} +export type Daemon = { + wait(): Promise + term(): Promise + [DaemonProof]: never +} + +export type HealthStatus = NamedHealthCheckResult["result"] +export type SmtpValue = { + server: string + port: number + from: string + login: string + password: string | null | undefined +} + +export type CommandType = string | [string, ...string[]] + +export type DaemonReturned = { + wait(): Promise + term(options?: { signal?: Signals; timeout?: number }): Promise +} + +export declare const hostName: unique symbol +// asdflkjadsf.onion | 1.2.3.4 +export type Hostname = string & { [hostName]: never } + +export type ServiceInterfaceId = string + +export { ServiceInterface } +export type ExposeServicePaths = { + /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ + paths: ExposedStorePaths +} + +export type EffectMethod = { + [K in keyof T]-?: K extends string + ? T[K] extends Function + ? ToKebab + : T[K] extends StringObject + ? `${ToKebab}.${EffectMethod}` + : never + : never +}[keyof T] + +export type SyncOptions = { + /** delete files that exist in the target directory, but not in the source directory */ + delete: boolean + /** do not sync files with paths that match these patterns */ + exclude: string[] +} + +/** + * This is the metadata that is returned from the metadata call. + */ +export type Metadata = { + fileType: string + isDir: boolean + isFile: boolean + isSymlink: boolean + len: number + modified?: Date + accessed?: Date + created?: Date + readonly: boolean + uid: number + gid: number + mode: number +} + +export type SetResult = { + dependsOn: DependsOn + signal: Signals +} + +export type PackageId = string +export type Message = string +export type DependencyKind = "running" | "exists" + +export type DependsOn = { + [packageId: string]: string[] | readonly string[] +} + +export type KnownError = + | { error: string } + | { + errorCode: [number, string] | readonly [number, string] + } + +export type Dependencies = Array + +export type DeepPartial = T extends unknown[] + ? T + : T extends {} + ? { [P in keyof T]?: DeepPartial } + : T diff --git a/sdk/base/lib/types/ManifestTypes.ts b/sdk/base/lib/types/ManifestTypes.ts new file mode 100644 index 000000000..17defebcd --- /dev/null +++ b/sdk/base/lib/types/ManifestTypes.ts @@ -0,0 +1,168 @@ +import { T } from ".." +import { ImageId, ImageSource } from "../types" + +export type SDKManifest = { + /** + * The package identifier used by StartOS. This must be unique amongst all other known packages. + * @example nextcloud + * */ + readonly id: string + /** + * The human readable name of your service + * @example Nextcloud + * */ + readonly title: string + /** + * The name of the software license for this project. The license itself should be included in the root of the project directory. + * @example MIT + */ + readonly license: string + /** + * URL of the StartOS package repository + * @example `https://github.com/Start9Labs/nextcloud-startos` + */ + readonly wrapperRepo: string + /** + * URL of the upstream service repository + * @example `https://github.com/nextcloud/docker` + */ + readonly upstreamRepo: string + /** + * URL where users can get help using the upstream service + * @example `https://github.com/nextcloud/docker/issues` + */ + readonly supportSite: string + /** + * URL where users can learn more about the upstream service + * @example `https://nextcloud.com` + */ + readonly marketingSite: string + /** + * (optional) URL where users can donate to the upstream project + * @example `https://nextcloud.com/contribute/` + */ + readonly donationUrl: string | null + readonly description: { + /** Short description to display on the marketplace list page. Max length 80 chars. */ + readonly short: string + /** Long description to display on the marketplace details page for this service. Max length 500 chars. */ + readonly long: string + } + /** + * @description A mapping of OS images needed to run the container processes. Each image ID is a unique key. + * @example + * Using dockerTag... + * + * ``` + images: { + main: { + source: { + dockerTag: 'start9/hello-world', + }, + }, + }, + * ``` + * @example + * Using dockerBuild... + * + * ``` + images: { + main: { + source: { + dockerBuild: { + dockerFile: '../Dockerfile', + workdir: '.', + }, + }, + }, + }, + * ``` + */ + readonly images: Record + /** + * @description A list of readonly asset directories that will mount to the container. Each item here must + * correspond to a directory in the /assets directory of this project. + * + * Most projects will not make use of this. + * @example [] + */ + readonly assets: string[] + /** + * @description A list of data volumes that will mount to the container. Must contain at least one volume. + * @example ['main'] + */ + readonly volumes: string[] + + readonly alerts?: { + /** An warning alert requiring user confirmation before proceeding with initial installation of this service. */ + readonly install?: string | null + /** An warning alert requiring user confirmation before updating this service. */ + readonly update?: string | null + /** An warning alert requiring user confirmation before uninstalling this service. */ + readonly uninstall?: string | null + /** An warning alert requiring user confirmation before restoring this service from backup. */ + readonly restore?: string | null + /** An warning alert requiring user confirmation before starting this service. */ + readonly start?: string | null + /** An warning alert requiring user confirmation before stopping this service. */ + readonly stop?: string | null + } + /** + * @description A mapping of service dependencies to be displayed to users when viewing the Marketplace + * @property {string} description - An explanation of why this service is a dependency. + * @property {boolean} optional - Whether or not this dependency is required or contingent on user configuration. + * @property {string} s9pk - A path or url to an s9pk of the dependency to extract metadata at build time + * @example + * ``` + dependencies: { + 'hello-world': { + description: 'A moon needs a world', + optional: false, + s9pk: '', + }, + }, + * ``` + */ + readonly dependencies: Record + /** + * @description (optional) A set of hardware requirements for this service. If the user's machine + * does not meet these requirements, they will not be able to install this service. + * @property {object[]} devices - TODO Aiden confirm type on the left. List of required devices (displays or processors). + * @property {number} ram - Minimum RAM requirement (in megabytes MB) + * @property {string[]} arch - List of supported arches + * @example + * ``` + TODO Aiden verify below and provide examples for devices + hardwareRequirements: { + devices: [ + { class: 'display', value: '' }, + { class: 'processor', value: '' }, + ], + ram: 8192, + arch: ['x86-64'], + }, + * ``` + */ + readonly hardwareRequirements?: { + readonly device?: T.DeviceFilter[] + readonly ram?: number | null + readonly arch?: string[] | null + } +} + +// this is hacky but idk a more elegant way +type ArchOptions = { + 0: ["x86_64", "aarch64"] + 1: ["aarch64", "x86_64"] + 2: ["x86_64"] + 3: ["aarch64"] +} +export type SDKImageInputSpec = { + [A in keyof ArchOptions]: { + source: Exclude + arch?: ArchOptions[A] + emulateMissingAs?: ArchOptions[A][number] | null + } +}[keyof ArchOptions] + +export type ManifestDependency = T.Manifest["dependencies"][string] diff --git a/sdk/base/lib/util/GetSystemSmtp.ts b/sdk/base/lib/util/GetSystemSmtp.ts new file mode 100644 index 000000000..cd87a9db6 --- /dev/null +++ b/sdk/base/lib/util/GetSystemSmtp.ts @@ -0,0 +1,35 @@ +import { Effects } from "../Effects" + +export class GetSystemSmtp { + constructor(readonly effects: Effects) {} + + /** + * Returns the system SMTP credentials. Restarts the service if the credentials change + */ + const() { + return this.effects.getSystemSmtp({ + callback: () => this.effects.constRetry(), + }) + } + /** + * Returns the system SMTP credentials. Does nothing if the credentials change + */ + once() { + return this.effects.getSystemSmtp({}) + } + /** + * Watches the system SMTP credentials. Takes a custom callback function to run whenever the credentials change + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.getSystemSmtp({ + callback: () => callback(), + }) + await waitForNext + } + } +} diff --git a/sdk/base/lib/util/Hostname.ts b/sdk/base/lib/util/Hostname.ts new file mode 100644 index 000000000..ee68abe4f --- /dev/null +++ b/sdk/base/lib/util/Hostname.ts @@ -0,0 +1,25 @@ +import { HostnameInfo } from "../types" + +export function hostnameInfoToAddress(hostInfo: HostnameInfo): string { + if (hostInfo.kind === "onion") { + return `${hostInfo.hostname.value}` + } + if (hostInfo.kind !== "ip") { + throw Error("Expecting that the kind is ip.") + } + const hostname = hostInfo.hostname + if (hostname.kind === "domain") { + return `${hostname.subdomain ? `${hostname.subdomain}.` : ""}${hostname.domain}` + } + const port = hostname.sslPort || hostname.port + const portString = port ? `:${port}` : "" + if ("ipv4" === hostname.kind || "ipv6" === hostname.kind) { + return `${hostname.value}${portString}` + } + if ("local" === hostname.kind) { + return `${hostname.value}${portString}` + } + throw Error( + "Expecting to have a valid hostname kind." + JSON.stringify(hostname), + ) +} diff --git a/sdk/base/lib/util/PathBuilder.ts b/sdk/base/lib/util/PathBuilder.ts new file mode 100644 index 000000000..038fa5ac2 --- /dev/null +++ b/sdk/base/lib/util/PathBuilder.ts @@ -0,0 +1,38 @@ +import { Affine } from "../util" + +const pathValue = Symbol("pathValue") +export type PathValue = typeof pathValue + +export type PathBuilderStored = { + [K in PathValue]: [AllStore, Store] +} + +export type PathBuilder = (Store extends Record< + string, + unknown +> + ? { + [K in keyof Store]: PathBuilder + } + : {}) & + PathBuilderStored + +export type StorePath = string & Affine<"StorePath"> +const privateSymbol = Symbol("jsonPath") +export const extractJsonPath = (builder: PathBuilder) => { + return (builder as any)[privateSymbol] as StorePath +} + +export const pathBuilder = ( + paths: string[] = [], +): PathBuilder => { + return new Proxy({} as PathBuilder, { + get(target, prop) { + if (prop === privateSymbol) { + if (paths.length === 0) return "" + return `/${paths.join("/")}` + } + return pathBuilder([...paths, prop as string]) + }, + }) as PathBuilder +} diff --git a/sdk/base/lib/util/asError.ts b/sdk/base/lib/util/asError.ts new file mode 100644 index 000000000..c3454e0e4 --- /dev/null +++ b/sdk/base/lib/util/asError.ts @@ -0,0 +1,9 @@ +export const asError = (e: unknown) => { + if (e instanceof Error) { + return new Error(e as any) + } + if (typeof e === "string") { + return new Error(`${e}`) + } + return new Error(`${JSON.stringify(e)}`) +} diff --git a/sdk/base/lib/util/deepEqual.ts b/sdk/base/lib/util/deepEqual.ts new file mode 100644 index 000000000..8e6ba4b65 --- /dev/null +++ b/sdk/base/lib/util/deepEqual.ts @@ -0,0 +1,19 @@ +import { object } from "ts-matches" + +export function deepEqual(...args: unknown[]) { + if (!object.test(args[args.length - 1])) return args[args.length - 1] + const objects = args.filter(object.test) + if (objects.length === 0) { + for (const x of args) if (x !== args[0]) return false + return true + } + if (objects.length !== args.length) return false + const allKeys = new Set(objects.flatMap((x) => Object.keys(x))) + for (const key of allKeys) { + for (const x of objects) { + if (!(key in x)) return false + if (!deepEqual((objects[0] as any)[key], (x as any)[key])) return false + } + } + return true +} diff --git a/sdk/base/lib/util/deepMerge.ts b/sdk/base/lib/util/deepMerge.ts new file mode 100644 index 000000000..72392a887 --- /dev/null +++ b/sdk/base/lib/util/deepMerge.ts @@ -0,0 +1,86 @@ +export function partialDiff( + prev: T, + next: T, +): { diff: Partial } | undefined { + if (prev === next) { + return + } else if (Array.isArray(prev) && Array.isArray(next)) { + const res = { diff: [] as any[] } + for (let newItem of next) { + let anyEq = false + for (let oldItem of prev) { + if (!partialDiff(oldItem, newItem)) { + anyEq = true + break + } + } + if (!anyEq) { + res.diff.push(newItem) + } + } + if (res.diff.length) { + return res as any + } else { + return + } + } else if (typeof prev === "object" && typeof next === "object") { + if (prev === null) { + return { diff: next } + } + if (next === null) return + const res = { diff: {} as Record } + for (let key in next) { + const diff = partialDiff(prev[key], next[key]) + if (diff) { + res.diff[key] = diff.diff + } + } + if (Object.keys(res.diff).length) { + return res + } else { + return + } + } else { + return { diff: next } + } +} + +export function deepMerge(...args: unknown[]): unknown { + const lastItem = (args as any)[args.length - 1] + if (typeof lastItem !== "object" || !lastItem) return lastItem + if (Array.isArray(lastItem)) + return deepMergeList( + ...(args.filter((x) => Array.isArray(x)) as unknown[][]), + ) + return deepMergeObject( + ...(args.filter( + (x) => typeof x === "object" && x && !Array.isArray(x), + ) as object[]), + ) +} + +function deepMergeList(...args: unknown[][]): unknown[] { + const res: unknown[] = [] + for (let arg of args) { + for (let item of arg) { + if (!res.some((x) => !partialDiff(x, item))) { + res.push(item) + } + } + } + return res +} + +function deepMergeObject(...args: object[]): object { + const lastItem = (args as any)[args.length - 1] + if (args.length === 0) return lastItem as any + if (args.length === 1) args.unshift({}) + const allKeys = new Set(args.flatMap((x) => Object.keys(x))) + for (const key of allKeys) { + const filteredValues = args.flatMap((x) => + key in x ? [(x as any)[key]] : [], + ) + ;(args as any)[0][key] = deepMerge(...filteredValues) + } + return args[0] as any +} diff --git a/sdk/base/lib/util/getDefaultString.ts b/sdk/base/lib/util/getDefaultString.ts new file mode 100644 index 000000000..2bbf8d279 --- /dev/null +++ b/sdk/base/lib/util/getDefaultString.ts @@ -0,0 +1,10 @@ +import { DefaultString } from "../actions/input/inputSpecTypes" +import { getRandomString } from "./getRandomString" + +export function getDefaultString(defaultSpec: DefaultString): string { + if (typeof defaultSpec === "string") { + return defaultSpec + } else { + return getRandomString(defaultSpec) + } +} diff --git a/sdk/base/lib/util/getRandomCharInSet.ts b/sdk/base/lib/util/getRandomCharInSet.ts new file mode 100644 index 000000000..7914209a3 --- /dev/null +++ b/sdk/base/lib/util/getRandomCharInSet.ts @@ -0,0 +1,99 @@ +// a,g,h,A-Z,,,,- + +export function getRandomCharInSet(charset: string): string { + const set = stringToCharSet(charset) + let charIdx = Math.floor( + (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32) * set.len, + ) + for (let range of set.ranges) { + if (range.len > charIdx) { + return String.fromCharCode(range.start.charCodeAt(0) + charIdx) + } + charIdx -= range.len + } + throw new Error("unreachable") +} +function stringToCharSet(charset: string): CharSet { + let set: CharSet = { ranges: [], len: 0 } + let start: string | null = null + let end: string | null = null + let in_range = false + for (let char of charset) { + switch (char) { + case ",": + if (start !== null && end !== null) { + if (start!.charCodeAt(0) > end!.charCodeAt(0)) { + throw new Error("start > end of charset") + } + const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 + set.ranges.push({ + start, + end, + len, + }) + set.len += len + start = null + end = null + in_range = false + } else if (start !== null && !in_range) { + set.len += 1 + set.ranges.push({ start, end: start, len: 1 }) + start = null + } else if (start !== null && in_range) { + end = "," + } else if (start === null && end === null && !in_range) { + start = "," + } else { + throw new Error('unexpected ","') + } + break + case "-": + if (start === null) { + start = "-" + } else if (!in_range) { + in_range = true + } else if (in_range && end === null) { + end = "-" + } else { + throw new Error('unexpected "-"') + } + break + default: + if (start === null) { + start = char + } else if (in_range && end === null) { + end = char + } else { + throw new Error(`unexpected "${char}"`) + } + } + } + if (start !== null && end !== null) { + if (start!.charCodeAt(0) > end!.charCodeAt(0)) { + throw new Error("start > end of charset") + } + const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 + set.ranges.push({ + start, + end, + len, + }) + set.len += len + } else if (start !== null) { + set.len += 1 + set.ranges.push({ + start, + end: start, + len: 1, + }) + } + return set +} +type CharSet = { + ranges: { + start: string + end: string + len: number + }[] + len: number +} diff --git a/sdk/base/lib/util/getRandomString.ts b/sdk/base/lib/util/getRandomString.ts new file mode 100644 index 000000000..7b52041d8 --- /dev/null +++ b/sdk/base/lib/util/getRandomString.ts @@ -0,0 +1,11 @@ +import { RandomString } from "../actions/input/inputSpecTypes" +import { getRandomCharInSet } from "./getRandomCharInSet" + +export function getRandomString(generator: RandomString): string { + let s = "" + for (let i = 0; i < generator.len; i++) { + s = s + getRandomCharInSet(generator.charset) + } + + return s +} diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts new file mode 100644 index 000000000..2e81e5ee2 --- /dev/null +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -0,0 +1,261 @@ +import { ServiceInterfaceType } from "../types" +import { knownProtocols } from "../interfaces/Host" +import { AddressInfo, Host, Hostname, HostnameInfo } from "../types" +import { Effects } from "../Effects" + +export type UrlString = string +export type HostId = string + +const getHostnameRegex = /^(\w+:\/\/)?([^\/\:]+)(:\d{1,3})?(\/)?/ +export const getHostname = (url: string): Hostname | null => { + const founds = url.match(getHostnameRegex)?.[2] + if (!founds) return null + const parts = founds.split("@") + const last = parts[parts.length - 1] as Hostname | null + return last +} + +export type Filled = { + hostnames: HostnameInfo[] + onionHostnames: HostnameInfo[] + localHostnames: HostnameInfo[] + ipHostnames: HostnameInfo[] + ipv4Hostnames: HostnameInfo[] + ipv6Hostnames: HostnameInfo[] + nonIpHostnames: HostnameInfo[] + + urls: UrlString[] + onionUrls: UrlString[] + localUrls: UrlString[] + ipUrls: UrlString[] + ipv4Urls: UrlString[] + ipv6Urls: UrlString[] + nonIpUrls: UrlString[] +} +export type FilledAddressInfo = AddressInfo & Filled +export type ServiceInterfaceFilled = { + id: string + /** The title of this field to be displayed */ + name: string + /** Human readable description, used as tooltip usually */ + description: string + /** Whether or not to mask the URIs for this interface. Useful if the URIs contain sensitive information, such as a password, macaroon, or API key */ + masked: boolean + /** Information about the host for this binding */ + host: Host | null + /** URI information */ + addressInfo: FilledAddressInfo | null + /** Indicates if we are a ui/p2p/api for the kind of interface that this is representing */ + type: ServiceInterfaceType +} +const either = + (...args: ((a: A) => boolean)[]) => + (a: A) => + args.some((x) => x(a)) +const negate = + (fn: (a: A) => boolean) => + (a: A) => + !fn(a) +const unique = (values: A[]) => Array.from(new Set(values)) +export const addressHostToUrl = ( + { scheme, sslScheme, username, suffix }: AddressInfo, + host: HostnameInfo, +): UrlString[] => { + const res = [] + const fmt = (scheme: string | null, host: HostnameInfo, port: number) => { + const excludePort = + scheme && + scheme in knownProtocols && + port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort + let hostname + if (host.kind === "onion") { + hostname = host.hostname.value + } else if (host.kind === "ip") { + if (host.hostname.kind === "domain") { + hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}` + } else if (host.hostname.kind === "ipv6") { + hostname = host.hostname.value.startsWith("fe80::") + ? `[${host.hostname.value}%${host.hostname.scopeId}]` + : `[${host.hostname.value}]` + } else { + hostname = host.hostname.value + } + } + return `${scheme ? `${scheme}://` : ""}${ + username ? `${username}@` : "" + }${hostname}${excludePort ? "" : `:${port}`}${suffix}` + } + if (host.hostname.sslPort !== null) { + res.push(fmt(sslScheme, host, host.hostname.sslPort)) + } + if (host.hostname.port !== null) { + res.push(fmt(scheme, host, host.hostname.port)) + } + + return res +} + +export const filledAddress = ( + host: Host, + addressInfo: AddressInfo, +): FilledAddressInfo => { + const toUrl = addressHostToUrl.bind(null, addressInfo) + const hostnames = host.hostnameInfo[addressInfo.internalPort] + + return { + ...addressInfo, + hostnames, + get onionHostnames() { + return hostnames.filter((h) => h.kind === "onion") + }, + get localHostnames() { + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "local", + ) + }, + get ipHostnames() { + return hostnames.filter( + (h) => + h.kind === "ip" && + (h.hostname.kind === "ipv4" || h.hostname.kind === "ipv6"), + ) + }, + get ipv4Hostnames() { + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "ipv4", + ) + }, + get ipv6Hostnames() { + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "ipv6", + ) + }, + get nonIpHostnames() { + return hostnames.filter( + (h) => + h.kind === "ip" && + h.hostname.kind !== "ipv4" && + h.hostname.kind !== "ipv6", + ) + }, + get urls() { + return this.hostnames.flatMap(toUrl) + }, + get onionUrls() { + return this.onionHostnames.flatMap(toUrl) + }, + get localUrls() { + return this.localHostnames.flatMap(toUrl) + }, + get ipUrls() { + return this.ipHostnames.flatMap(toUrl) + }, + get ipv4Urls() { + return this.ipv4Hostnames.flatMap(toUrl) + }, + get ipv6Urls() { + return this.ipv6Hostnames.flatMap(toUrl) + }, + get nonIpUrls() { + return this.nonIpHostnames.flatMap(toUrl) + }, + } +} + +const makeInterfaceFilled = async ({ + effects, + id, + packageId, + callback, +}: { + effects: Effects + id: string + packageId?: string + callback?: () => void +}) => { + const serviceInterfaceValue = await effects.getServiceInterface({ + serviceInterfaceId: id, + packageId, + callback, + }) + if (!serviceInterfaceValue) { + return null + } + const hostId = serviceInterfaceValue.addressInfo.hostId + const host = await effects.getHostInfo({ + packageId, + hostId, + callback, + }) + + const interfaceFilled: ServiceInterfaceFilled = { + ...serviceInterfaceValue, + host, + addressInfo: host + ? filledAddress(host, serviceInterfaceValue.addressInfo) + : null, + } + return interfaceFilled +} + +export class GetServiceInterface { + constructor( + readonly effects: Effects, + readonly opts: { id: string; packageId?: string }, + ) {} + + /** + * Returns the value of Store at the provided path. Restart the service if the value changes + */ + async const() { + const { id, packageId } = this.opts + const callback = () => this.effects.constRetry() + const interfaceFilled = await makeInterfaceFilled({ + effects: this.effects, + id, + packageId, + callback, + }) + + return interfaceFilled + } + /** + * Returns the value of ServiceInterfacesFilled at the provided path. Does nothing if the value changes + */ + async once() { + const { id, packageId } = this.opts + const interfaceFilled = await makeInterfaceFilled({ + effects: this.effects, + id, + packageId, + }) + + return interfaceFilled + } + + /** + * Watches the value of ServiceInterfacesFilled at the provided path. Takes a custom callback function to run whenever the value changes + */ + async *watch() { + const { id, packageId } = this.opts + while (true) { + let callback: () => void = () => {} + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await makeInterfaceFilled({ + effects: this.effects, + id, + packageId, + callback, + }) + await waitForNext + } + } +} +export function getServiceInterface( + effects: Effects, + opts: { id: string; packageId?: string }, +) { + return new GetServiceInterface(effects, opts) +} diff --git a/sdk/base/lib/util/getServiceInterfaces.ts b/sdk/base/lib/util/getServiceInterfaces.ts new file mode 100644 index 000000000..faeb508b4 --- /dev/null +++ b/sdk/base/lib/util/getServiceInterfaces.ts @@ -0,0 +1,102 @@ +import { Effects } from "../Effects" +import { + ServiceInterfaceFilled, + filledAddress, + getHostname, +} from "./getServiceInterface" + +const makeManyInterfaceFilled = async ({ + effects, + packageId, + callback, +}: { + effects: Effects + packageId?: string + callback?: () => void +}) => { + const serviceInterfaceValues = await effects.listServiceInterfaces({ + packageId, + callback, + }) + + const serviceInterfacesFilled: ServiceInterfaceFilled[] = await Promise.all( + Object.values(serviceInterfaceValues).map(async (serviceInterfaceValue) => { + const hostId = serviceInterfaceValue.addressInfo.hostId + const host = await effects.getHostInfo({ + packageId, + hostId, + callback, + }) + if (!host) { + throw new Error(`host ${hostId} not found!`) + } + return { + ...serviceInterfaceValue, + host, + addressInfo: filledAddress(host, serviceInterfaceValue.addressInfo), + } + }), + ) + return serviceInterfacesFilled +} + +export class GetServiceInterfaces { + constructor( + readonly effects: Effects, + readonly opts: { packageId?: string }, + ) {} + + /** + * Returns the value of Store at the provided path. Restart the service if the value changes + */ + async const() { + const { packageId } = this.opts + const callback = () => this.effects.constRetry() + const interfaceFilled: ServiceInterfaceFilled[] = + await makeManyInterfaceFilled({ + effects: this.effects, + packageId, + callback, + }) + + return interfaceFilled + } + /** + * Returns the value of ServiceInterfacesFilled at the provided path. Does nothing if the value changes + */ + async once() { + const { packageId } = this.opts + const interfaceFilled: ServiceInterfaceFilled[] = + await makeManyInterfaceFilled({ + effects: this.effects, + packageId, + }) + + return interfaceFilled + } + + /** + * Watches the value of ServiceInterfacesFilled at the provided path. Takes a custom callback function to run whenever the value changes + */ + async *watch() { + const { packageId } = this.opts + while (true) { + let callback: () => void = () => {} + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await makeManyInterfaceFilled({ + effects: this.effects, + packageId, + callback, + }) + await waitForNext + } + } +} +export function getServiceInterfaces( + effects: Effects, + opts: { packageId?: string }, +) { + return new GetServiceInterfaces(effects, opts) +} diff --git a/sdk/base/lib/util/graph.ts b/sdk/base/lib/util/graph.ts new file mode 100644 index 000000000..682ccf63e --- /dev/null +++ b/sdk/base/lib/util/graph.ts @@ -0,0 +1,251 @@ +import { boolean } from "ts-matches" + +export type Vertex = { + metadata: VMetadata + edges: Array> +} + +export type Edge = { + metadata: EMetadata + from: Vertex + to: Vertex +} + +export class Graph { + private readonly vertices: Array> = [] + constructor() {} + addVertex( + metadata: VMetadata, + fromEdges: Array, "to">>, + toEdges: Array, "from">>, + ): Vertex { + const vertex: Vertex = { + metadata, + edges: [], + } + for (let edge of fromEdges) { + const vEdge = { + metadata: edge.metadata, + from: edge.from, + to: vertex, + } + edge.from.edges.push(vEdge) + vertex.edges.push(vEdge) + } + for (let edge of toEdges) { + const vEdge = { + metadata: edge.metadata, + from: vertex, + to: edge.to, + } + edge.to.edges.push(vEdge) + vertex.edges.push(vEdge) + } + this.vertices.push(vertex) + return vertex + } + findVertex( + predicate: (vertex: Vertex) => boolean, + ): Generator, null> { + const veritces = this.vertices + function* gen() { + for (let vertex of veritces) { + if (predicate(vertex)) { + yield vertex + } + } + return null + } + return gen() + } + addEdge( + metadata: EMetadata, + from: Vertex, + to: Vertex, + ): Edge { + const edge = { + metadata, + from, + to, + } + edge.from.edges.push(edge) + edge.to.edges.push(edge) + return edge + } + breadthFirstSearch( + from: + | Vertex + | ((vertex: Vertex) => boolean), + ): Generator, null> { + const visited: Array> = [] + function* rec( + vertex: Vertex, + ): Generator, null> { + if (visited.includes(vertex)) { + return null + } + visited.push(vertex) + yield vertex + let generators = vertex.edges + .filter((e) => e.from === vertex) + .map((e) => rec(e.to)) + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (!next.done) { + generators.push(gen) + yield next.value + } + } + } + return null + } + + if (from instanceof Function) { + let generators = this.vertices.filter(from).map(rec) + return (function* () { + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (!next.done) { + generators.push(gen) + yield next.value + } + } + } + return null + })() + } else { + return rec(from) + } + } + reverseBreadthFirstSearch( + to: + | Vertex + | ((vertex: Vertex) => boolean), + ): Generator, null> { + const visited: Array> = [] + function* rec( + vertex: Vertex, + ): Generator, null> { + if (visited.includes(vertex)) { + return null + } + visited.push(vertex) + yield vertex + let generators = vertex.edges + .filter((e) => e.to === vertex) + .map((e) => rec(e.from)) + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (!next.done) { + generators.push(gen) + yield next.value + } + } + } + return null + } + + if (to instanceof Function) { + let generators = this.vertices.filter(to).map(rec) + return (function* () { + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (!next.done) { + generators.push(gen) + yield next.value + } + } + } + return null + })() + } else { + return rec(to) + } + } + shortestPath( + from: + | Vertex + | ((vertex: Vertex) => boolean), + to: + | Vertex + | ((vertex: Vertex) => boolean), + ): Array> | null { + const isDone = + to instanceof Function + ? to + : (v: Vertex) => v === to + const path: Array> = [] + const visited: Array> = [] + function* check( + vertex: Vertex, + path: Array>, + ): Generator> | null> { + if (isDone(vertex)) { + return path + } + if (visited.includes(vertex)) { + return null + } + visited.push(vertex) + yield + let generators = vertex.edges + .filter((e) => e.from === vertex) + .map((e) => check(e.to, [...path, e])) + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (next.done === true) { + if (next.value) { + return next.value + } + } else { + generators.push(gen) + yield + } + } + } + return null + } + + if (from instanceof Function) { + let generators = this.vertices.filter(from).map((v) => check(v, [])) + while (generators.length) { + let prev = generators + generators = [] + for (let gen of prev) { + const next = gen.next() + if (next.done === true) { + if (next.value) { + return next.value + } + } else { + generators.push(gen) + } + } + } + } else { + const gen = check(from, []) + while (true) { + const next = gen.next() + if (next.done) { + return next.value + } + } + } + return null + } +} diff --git a/sdk/base/lib/util/inMs.test.ts b/sdk/base/lib/util/inMs.test.ts new file mode 100644 index 000000000..fbf71bf2c --- /dev/null +++ b/sdk/base/lib/util/inMs.test.ts @@ -0,0 +1,34 @@ +import { inMs } from "./inMs" + +describe("inMs", () => { + test("28.001s", () => { + expect(inMs("28.001s")).toBe(28001) + }) + test("28.123s", () => { + expect(inMs("28.123s")).toBe(28123) + }) + test(".123s", () => { + expect(inMs(".123s")).toBe(123) + }) + test("123ms", () => { + expect(inMs("123ms")).toBe(123) + }) + test("1h", () => { + expect(inMs("1h")).toBe(3600000) + }) + test("1m", () => { + expect(inMs("1m")).toBe(60000) + }) + test("1m", () => { + expect(inMs("1d")).toBe(1000 * 60 * 60 * 24) + }) + test("123", () => { + expect(() => inMs("123")).toThrowError("Invalid time format: 123") + }) + test("123 as number", () => { + expect(inMs(123)).toBe(123) + }) + test.only("undefined", () => { + expect(inMs(undefined)).toBe(undefined) + }) +}) diff --git a/sdk/base/lib/util/inMs.ts b/sdk/base/lib/util/inMs.ts new file mode 100644 index 000000000..548eb14bf --- /dev/null +++ b/sdk/base/lib/util/inMs.ts @@ -0,0 +1,29 @@ +const matchTimeRegex = /^\s*(\d+)?(\.\d+)?\s*(ms|s|m|h|d)/ + +const unitMultiplier = (unit?: string) => { + if (!unit) return 1 + if (unit === "ms") return 1 + if (unit === "s") return 1000 + if (unit === "m") return 1000 * 60 + if (unit === "h") return 1000 * 60 * 60 + if (unit === "d") return 1000 * 60 * 60 * 24 + throw new Error(`Invalid unit: ${unit}`) +} +const digitsMs = (digits: string | null, multiplier: number) => { + if (!digits) return 0 + const value = parseInt(digits.slice(1)) + const divideBy = multiplier / Math.pow(10, digits.length - 1) + return Math.round(value * divideBy) +} +export const inMs = (time?: string | number) => { + if (typeof time === "number") return time + if (!time) return undefined + const matches = time.match(matchTimeRegex) + if (!matches) throw new Error(`Invalid time format: ${time}`) + const [_, leftHandSide, digits, unit] = matches + const multiplier = unitMultiplier(unit) + const firstValue = parseInt(leftHandSide || "0") * multiplier + const secondValue = digitsMs(digits, multiplier) + + return firstValue + secondValue +} diff --git a/sdk/base/lib/util/index.ts b/sdk/base/lib/util/index.ts new file mode 100644 index 000000000..4c9e803bb --- /dev/null +++ b/sdk/base/lib/util/index.ts @@ -0,0 +1,22 @@ +/// Currently being used +export { addressHostToUrl } from "./getServiceInterface" +export { getDefaultString } from "./getDefaultString" + +/// Not being used, but known to be browser compatible +export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" +export { getServiceInterfaces } from "./getServiceInterfaces" +export { once } from "./once" +export { asError } from "./asError" +export * as Patterns from "./patterns" +export * from "./typeHelpers" +export { GetSystemSmtp } from "./GetSystemSmtp" +export { Graph, Vertex } from "./graph" +export { inMs } from "./inMs" +export { splitCommand } from "./splitCommand" +export { nullIfEmpty } from "./nullIfEmpty" +export { deepMerge, partialDiff } from "./deepMerge" +export { deepEqual } from "./deepEqual" +export { hostnameInfoToAddress } from "./Hostname" +export { PathBuilder, extractJsonPath, StorePath } from "./PathBuilder" +export * as regexes from "./regexes" +export { stringFromStdErrOut } from "./stringFromStdErrOut" diff --git a/sdk/base/lib/util/nullIfEmpty.ts b/sdk/base/lib/util/nullIfEmpty.ts new file mode 100644 index 000000000..b24907b7d --- /dev/null +++ b/sdk/base/lib/util/nullIfEmpty.ts @@ -0,0 +1,10 @@ +/** + * A useful tool when doing a getInputSpec. + * Look into the inputSpec {@link FileHelper} for an example of the use. + * @param s + * @returns + */ +export function nullIfEmpty>(s: null | A) { + if (s === null) return null + return Object.keys(s).length === 0 ? null : s +} diff --git a/sdk/base/lib/util/once.ts b/sdk/base/lib/util/once.ts new file mode 100644 index 000000000..5f689b0e1 --- /dev/null +++ b/sdk/base/lib/util/once.ts @@ -0,0 +1,9 @@ +export function once(fn: () => B): () => B { + let result: [B] | [] = [] + return () => { + if (!result.length) { + result = [fn()] + } + return result[0] + } +} diff --git a/sdk/base/lib/util/patterns.ts b/sdk/base/lib/util/patterns.ts new file mode 100644 index 000000000..a61e4269c --- /dev/null +++ b/sdk/base/lib/util/patterns.ts @@ -0,0 +1,69 @@ +import { Pattern } from "../actions/input/inputSpecTypes" +import * as regexes from "./regexes" + +export const ipv6: Pattern = { + regex: regexes.ipv6.matches(), + description: "Must be a valid IPv6 address", +} + +export const ipv4: Pattern = { + regex: regexes.ipv4.matches(), + description: "Must be a valid IPv4 address", +} + +export const hostname: Pattern = { + regex: regexes.hostname.matches(), + description: "Must be a valid hostname", +} + +export const localHostname: Pattern = { + regex: regexes.localHostname.matches(), + description: 'Must be a valid ".local" hostname', +} + +export const torHostname: Pattern = { + regex: regexes.torHostname.matches(), + description: 'Must be a valid Tor (".onion") hostname', +} + +export const url: Pattern = { + regex: regexes.url.matches(), + description: "Must be a valid URL", +} + +export const localUrl: Pattern = { + regex: regexes.localUrl.matches(), + description: 'Must be a valid ".local" URL', +} + +export const torUrl: Pattern = { + regex: regexes.torUrl.matches(), + description: 'Must be a valid Tor (".onion") URL', +} + +export const ascii: Pattern = { + regex: regexes.ascii.matches(), + description: + "May only contain ASCII characters. See https://www.w3schools.com/charsets/ref_html_ascii.asp", +} + +export const domain: Pattern = { + regex: regexes.domain.matches(), + description: "Must be a valid Fully Qualified Domain Name", +} + +export const email: Pattern = { + regex: regexes.email.matches(), + description: "Must be a valid email address", +} + +export const emailWithName: Pattern = { + regex: regexes.emailWithName.matches(), + description: "Must be a valid email address, optionally with a name", +} + +export const base64: Pattern = { + regex: regexes.base64.matches(), + description: + "May only contain base64 characters. See https://base64.guru/learn/base64-characters", +} diff --git a/sdk/base/lib/util/regexes.ts b/sdk/base/lib/util/regexes.ts new file mode 100644 index 000000000..a2a8cde7a --- /dev/null +++ b/sdk/base/lib/util/regexes.ts @@ -0,0 +1,71 @@ +export class ComposableRegex { + readonly regex: RegExp + constructor(regex: RegExp | string) { + if (regex instanceof RegExp) { + this.regex = regex + } else { + this.regex = new RegExp(regex) + } + } + asExpr(): string { + return `(${this.regex.source})` + } + matches(): string { + return `^${this.regex.source}$` + } + contains(): string { + return this.regex.source + } +} + +// https://ihateregex.io/expr/ipv6/ +export const ipv6 = new ComposableRegex( + /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/, +) + +// https://ihateregex.io/expr/ipv4/ +export const ipv4 = new ComposableRegex( + /(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/, +) + +export const hostname = new ComposableRegex( + /(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/, +) + +export const localHostname = new ComposableRegex( + /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/, +) + +export const torHostname = new ComposableRegex( + /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/, +) + +// https://ihateregex.io/expr/url/ +export const url = new ComposableRegex( + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, +) + +export const localUrl = new ComposableRegex( + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, +) + +export const torUrl = new ComposableRegex( + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, +) + +// https://ihateregex.io/expr/ascii/ +export const ascii = new ComposableRegex(/[ -~]*/) + +export const domain = new ComposableRegex(/[A-Za-z0-9.-]+\.[A-Za-z]{2,}/) + +// https://www.regular-expressions.info/email.html +export const email = new ComposableRegex(`[A-Za-z0-9._%+-]+@${domain.asExpr()}`) + +export const emailWithName = new ComposableRegex( + `${email.asExpr()}|([^<]*<${email.asExpr()}>)`, +) + +//https://rgxdb.com/r/1NUN74O6 +export const base64 = new ComposableRegex( + /(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/, +) diff --git a/sdk/base/lib/util/splitCommand.ts b/sdk/base/lib/util/splitCommand.ts new file mode 100644 index 000000000..ac1237574 --- /dev/null +++ b/sdk/base/lib/util/splitCommand.ts @@ -0,0 +1,8 @@ +import { arrayOf, string } from "ts-matches" + +export const splitCommand = ( + command: string | [string, ...string[]], +): string[] => { + if (arrayOf(string).test(command)) return command + return ["sh", "-c", command] +} diff --git a/sdk/base/lib/util/stringFromStdErrOut.ts b/sdk/base/lib/util/stringFromStdErrOut.ts new file mode 100644 index 000000000..452aaa029 --- /dev/null +++ b/sdk/base/lib/util/stringFromStdErrOut.ts @@ -0,0 +1,6 @@ +export async function stringFromStdErrOut(x: { + stdout: string + stderr: string +}) { + return x?.stderr ? Promise.reject(x.stderr) : x.stdout +} diff --git a/sdk/base/lib/util/typeHelpers.ts b/sdk/base/lib/util/typeHelpers.ts new file mode 100644 index 000000000..d29d5c986 --- /dev/null +++ b/sdk/base/lib/util/typeHelpers.ts @@ -0,0 +1,116 @@ +import * as T from "../types" + +// prettier-ignore +export type FlattenIntersection = +T extends ArrayLike ? T : +T extends object ? {} & {[P in keyof T]: T[P]} : + T; + +export type _ = FlattenIntersection + +export const isKnownError = (e: unknown): e is T.KnownError => + e instanceof Object && ("error" in e || "error-code" in e) + +declare const affine: unique symbol + +export type Affine = { [affine]: A } + +type NeverPossible = { [affine]: string } +export type NoAny = NeverPossible extends A + ? keyof NeverPossible extends keyof A + ? never + : A + : A + +type CapitalLetters = + | "A" + | "B" + | "C" + | "D" + | "E" + | "F" + | "G" + | "H" + | "I" + | "J" + | "K" + | "L" + | "M" + | "N" + | "O" + | "P" + | "Q" + | "R" + | "S" + | "T" + | "U" + | "V" + | "W" + | "X" + | "Y" + | "Z" + +type Numbers = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + +type CapitalChars = CapitalLetters | Numbers + +export type ToKebab = S extends string + ? S extends `${infer Head}${CapitalChars}${infer Tail}` // string has a capital char somewhere + ? Head extends "" // there is a capital char in the first position + ? Tail extends "" + ? Lowercase /* 'A' */ + : S extends `${infer Caps}${Tail}` // tail exists, has capital characters + ? Caps extends CapitalChars + ? Tail extends CapitalLetters + ? `${Lowercase}-${Lowercase}` /* 'AB' */ + : Tail extends `${CapitalLetters}${string}` + ? `${ToKebab}-${ToKebab}` /* first tail char is upper? 'ABcd' */ + : `${ToKebab}${ToKebab}` /* 'AbCD','AbcD', */ /* TODO: if tail is only numbers, append without underscore */ + : never /* never reached, used for inference of caps */ + : never + : Tail extends "" /* 'aB' 'abCD' 'ABCD' 'AB' */ + ? S extends `${Head}${infer Caps}` + ? Caps extends CapitalChars + ? Head extends Lowercase /* 'abcD' */ + ? Caps extends Numbers + ? // Head exists and is lowercase, tail does not, Caps is a number, we may be in a sub-select + // if head ends with number, don't split head an Caps, keep contiguous numbers together + Head extends `${string}${Numbers}` + ? never + : // head does not end in number, safe to split. 'abc2' -> 'abc-2' + `${ToKebab}-${Caps}` + : `${ToKebab}-${ToKebab}` /* 'abcD' 'abc25' */ + : never /* stop union type forming */ + : never + : never /* never reached, used for inference of caps */ + : S extends `${Head}${infer Caps}${Tail}` /* 'abCd' 'ABCD' 'AbCd' 'ABcD' */ + ? Caps extends CapitalChars + ? Head extends Lowercase /* is 'abCd' 'abCD' ? */ + ? Tail extends CapitalLetters /* is 'abCD' where Caps = 'C' */ + ? `${ToKebab}-${ToKebab}-${Lowercase}` /* aBCD Tail = 'D', Head = 'aB' */ + : Tail extends `${CapitalLetters}${string}` /* is 'aBCd' where Caps = 'B' */ + ? Head extends Numbers + ? never /* stop union type forming */ + : Head extends `${string}${Numbers}` + ? never /* stop union type forming */ + : `${Head}-${ToKebab}-${ToKebab}` /* 'aBCd' => `${'a'}-${Lowercase<'B'>}-${ToSnake<'Cd'>}` */ + : `${ToKebab}-${Lowercase}${ToKebab}` /* 'aBcD' where Caps = 'B' tail starts as lowercase */ + : never + : never + : never + : S /* 'abc' */ + : never + +export type StringObject = Record + +function test() { + // prettier-ignore + const t = (a: ( + A extends B ? ( + B extends A ? null : never + ) : never + )) =>{ } + t<"foo-bar", ToKebab<"FooBar">>(null) + // @ts-expect-error + t<"foo-3ar", ToKebab<"FooBar">>(null) +} diff --git a/sdk/base/package-lock.json b/sdk/base/package-lock.json new file mode 100644 index 000000000..4d5625489 --- /dev/null +++ b/sdk/base/package-lock.json @@ -0,0 +1,4693 @@ +{ + "name": "@start9labs/start-sdk-base", + "version": "0.3.6-alpha8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@start9labs/start-sdk-base", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "@types/mime-types": "^2.1.4", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", + "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz", + "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.21.3", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.3", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.3", + "@babel/types": "^7.21.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", + "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", + "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", + "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", + "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", + "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.21.3", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.3", + "@babel/types": "^7.21.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", + "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "dependencies": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.15.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", + "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==", + "dev": true + }, + "node_modules/@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001470", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001470.tgz", + "integrity": "sha512-065uNwY6QtHCBOExzbV6m236DDhYCCtPmQUCoQtwkVqzud8v5QPidoMr6CoMkC2nfp6nksjttqWQRRh75LqUmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.341", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.341.tgz", + "integrity": "sha512-R4A8VfUBQY9WmAhuqY5tjHRf5fH2AAf6vqitBOE0y6u2PgHgqHSrhZmu78dIX3fVZtjqlwJNX1i2zwC3VpHtQQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-fetch/node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/peggy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-3.0.2.tgz", + "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^10.0.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", + "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-jest": { + "version": "29.0.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", + "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-matches": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", + "license": "MIT" + }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-pegjs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ts-pegjs/-/ts-pegjs-4.2.1.tgz", + "integrity": "sha512-mK/O2pu6lzWUeKpEMA/wsa0GdYblfjJI1y0s0GqH6xCTvugQDOWPJbm5rY6AHivpZICuXIriCb+a7Cflbdtc2w==", + "dev": true, + "dependencies": { + "prettier": "^2.8.8", + "ts-morph": "^18.0.0" + }, + "bin": { + "tspegjs": "dist/cli.mjs" + }, + "peerDependencies": { + "peggy": "^3.0.2" + } + }, + "node_modules/ts-pegjs/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", + "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", + "dev": true, + "dependencies": { + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sdk/base/package.json b/sdk/base/package.json new file mode 100644 index 000000000..6eae719a7 --- /dev/null +++ b/sdk/base/package.json @@ -0,0 +1,52 @@ +{ + "name": "@start9labs/start-sdk-base", + "main": "./index.js", + "types": "./index.d.ts", + "sideEffects": true, + "scripts": { + "peggy": "peggy --allowed-start-rules \"*\" --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs", + "test": "jest -c ./jest.config.js --coverage", + "buildOutput": "npx prettier --write \"**/*.ts\"", + "check": "tsc --noEmit", + "tsc": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Start9Labs/start-sdk.git" + }, + "author": "Start9 Labs", + "license": "MIT", + "bugs": { + "url": "https://github.com/Start9Labs/start-sdk/issues" + }, + "homepage": "https://github.com/Start9Labs/start-sdk#readme", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": false + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "@types/mime-types": "^2.1.4", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } +} diff --git a/sdk/base/tsconfig.json b/sdk/base/tsconfig.json new file mode 100644 index 000000000..cd73f3164 --- /dev/null +++ b/sdk/base/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "strict": true, + "preserveConstEnums": true, + "sourceMap": true, + "pretty": true, + "declaration": true, + "noImplicitAny": true, + "esModuleInterop": true, + "types": ["node", "jest"], + "moduleResolution": "node", + "skipLibCheck": true, + "module": "commonjs", + "outDir": "../baseDist", + "target": "es2018" + }, + "include": ["lib/**/*"], + "exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"] +} diff --git a/sdk/package/.gitignore b/sdk/package/.gitignore new file mode 100644 index 000000000..a7ca92b2d --- /dev/null +++ b/sdk/package/.gitignore @@ -0,0 +1,5 @@ +.vscode +dist/ +node_modules/ +lib/coverage +lib/test/output.ts \ No newline at end of file diff --git a/sdk/package/.npmignore b/sdk/package/.npmignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/sdk/package/.npmignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/sdk/package/.prettierignore b/sdk/package/.prettierignore new file mode 100644 index 000000000..19b24bbe8 --- /dev/null +++ b/sdk/package/.prettierignore @@ -0,0 +1 @@ +/lib/exver/exver.ts \ No newline at end of file diff --git a/sdk/package/LICENSE b/sdk/package/LICENSE new file mode 100644 index 000000000..793257b96 --- /dev/null +++ b/sdk/package/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Start9 Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/package/README.md b/sdk/package/README.md new file mode 100644 index 000000000..d51b25b58 --- /dev/null +++ b/sdk/package/README.md @@ -0,0 +1,18 @@ +# Start SDK + +## Config Conversion + +- Copy the old config json (from the getConfig.ts) +- Install the start-sdk with `npm i` +- paste the config into makeOutput.ts::oldSpecToBuilder (second param) +- Make the third param + +```ts + { + StartSdk: "start-sdk/lib", + } +``` + +- run the script `npm run buildOutput` to make the output.ts +- Copy this whole file into startos/procedures/config/spec.ts +- Fix all the TODO diff --git a/sdk/package/jest.config.js b/sdk/package/jest.config.js new file mode 100644 index 000000000..c38fa5062 --- /dev/null +++ b/sdk/package/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + automock: false, + testEnvironment: "node", + rootDir: "./lib/", + modulePathIgnorePatterns: ["./dist/"], +} diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts new file mode 100644 index 000000000..667e4297b --- /dev/null +++ b/sdk/package/lib/StartSdk.ts @@ -0,0 +1,1428 @@ +import { Value } from "../../base/lib/actions/input/builder/value" +import { + InputSpec, + ExtractInputSpecType, + LazyBuild, +} from "../../base/lib/actions/input/builder/inputSpec" +import { + DefaultString, + ListValueSpecText, + Pattern, + RandomString, + UniqueBy, + ValueSpecDatetime, + ValueSpecText, +} from "../../base/lib/actions/input/inputSpecTypes" +import { Variants } from "../../base/lib/actions/input/builder/variants" +import { Action, Actions } from "../../base/lib/actions/setupActions" +import { + SyncOptions, + ServiceInterfaceId, + PackageId, + HealthReceipt, + ServiceInterfaceType, + Effects, +} from "../../base/lib/types" +import * as patterns from "../../base/lib/util/patterns" +import { BackupSync, Backups } from "./backup/Backups" +import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants" +import { Daemons } from "./mainFn/Daemons" +import { healthCheck, HealthCheckParams } from "./health/HealthCheck" +import { checkPortListening } from "./health/checkFns/checkPortListening" +import { checkWebUrl, runHealthScript } from "./health/checkFns" +import { List } from "../../base/lib/actions/input/builder/list" +import { Install, InstallFn } from "./inits/setupInstall" +import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" +import { UninstallFn, setupUninstall } from "./inits/setupUninstall" +import { setupMain } from "./mainFn" +import { defaultTrigger } from "./trigger/defaultTrigger" +import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" +import { + UpdateServiceInterfaces, + setupServiceInterfaces, +} from "../../base/lib/interfaces/setupInterfaces" +import { successFailure } from "./trigger/successFailure" +import { MultiHost, Scheme } from "../../base/lib/interfaces/Host" +import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder" +import { GetSystemSmtp } from "./util" +import { nullIfEmpty } from "./util" +import { getServiceInterface, getServiceInterfaces } from "./util" +import { getStore } from "./store/getStore" +import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer" +import { splitCommand } from "./util" +import { Mounts } from "./mainFn/Mounts" +import { setupDependencies } from "../../base/lib/dependencies/setupDependencies" +import * as T from "../../base/lib/types" +import { testTypeVersion } from "../../base/lib/exver" +import { ExposedStorePaths } from "./store/setupExposeStore" +import { + PathBuilder, + extractJsonPath, + pathBuilder, +} from "../../base/lib/util/PathBuilder" +import { + CheckDependencies, + checkDependencies, +} from "../../base/lib/dependencies/dependencies" +import { GetSslCertificate } from "./util" +import { VersionGraph } from "./version" +import { MaybeFn } from "../../base/lib/actions/setupActions" +import { GetInput } from "../../base/lib/actions/setupActions" +import { Run } from "../../base/lib/actions/setupActions" +import * as actions from "../../base/lib/actions" +import { setupInit } from "./inits/setupInit" + +export const SDKVersion = testTypeVersion("0.3.6") + +// prettier-ignore +type AnyNeverCond = + T extends [] ? Else : + T extends [never, ...Array] ? Then : + T extends [any, ...infer U] ? AnyNeverCond : + never + +export class StartSdk { + private constructor(readonly manifest: Manifest) {} + static of() { + return new StartSdk(null as never) + } + withManifest(manifest: Manifest) { + return new StartSdk(manifest) + } + withStore>() { + return new StartSdk(this.manifest) + } + + build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { + type NestedEffects = "subcontainer" | "store" | "action" + type InterfaceEffects = + | "getServiceInterface" + | "listServiceInterfaces" + | "exportServiceInterface" + | "clearServiceInterfaces" + | "bind" + | "getHostInfo" + type MainUsedEffects = "setMainStatus" | "setHealth" + type CallbackEffects = "constRetry" | "clearCallbacks" + type AlreadyExposed = "getSslCertificate" | "getSystemSmtp" + + // prettier-ignore + type StartSdkEffectWrapper = { + [K in keyof Omit]: (effects: Effects, ...args: Parameters) => ReturnType + } + const startSdkEffectWrapper: StartSdkEffectWrapper = { + restart: (effects, ...args) => effects.restart(...args), + setDependencies: (effects, ...args) => effects.setDependencies(...args), + checkDependencies: (effects, ...args) => + effects.checkDependencies(...args), + mount: (effects, ...args) => effects.mount(...args), + getInstalledPackages: (effects, ...args) => + effects.getInstalledPackages(...args), + exposeForDependents: (effects, ...args) => + effects.exposeForDependents(...args), + getServicePortForward: (effects, ...args) => + effects.getServicePortForward(...args), + clearBindings: (effects, ...args) => effects.clearBindings(...args), + getContainerIp: (effects, ...args) => effects.getContainerIp(...args), + getSslKey: (effects, ...args) => effects.getSslKey(...args), + setDataVersion: (effects, ...args) => effects.setDataVersion(...args), + getDataVersion: (effects, ...args) => effects.getDataVersion(...args), + shutdown: (effects, ...args) => effects.shutdown(...args), + getDependencies: (effects, ...args) => effects.getDependencies(...args), + getStatus: (effects, ...args) => effects.getStatus(...args), + } + + return { + manifest: this.manifest, + ...startSdkEffectWrapper, + action: { + run: actions.runAction, + request: >( + effects: T.Effects, + packageId: T.PackageId, + action: T, + severity: T.ActionSeverity, + options?: actions.ActionRequestOptions, + ) => + actions.requestAction({ + effects, + packageId, + action, + severity, + options: options, + }), + requestOwn: >( + effects: T.Effects, + action: T, + severity: T.ActionSeverity, + options?: actions.ActionRequestOptions, + ) => + actions.requestAction({ + effects, + packageId: this.manifest.id, + action, + severity, + options: options, + }), + clearRequest: (effects: T.Effects, ...replayIds: string[]) => + effects.action.clearRequests({ only: replayIds }), + }, + checkDependencies: checkDependencies as < + DependencyId extends keyof Manifest["dependencies"] & + PackageId = keyof Manifest["dependencies"] & PackageId, + >( + effects: Effects, + packageIds?: DependencyId[], + ) => Promise>, + serviceInterface: { + getOwn: (effects: E, id: ServiceInterfaceId) => + getServiceInterface(effects, { + id, + }), + get: ( + effects: E, + opts: { id: ServiceInterfaceId; packageId: PackageId }, + ) => getServiceInterface(effects, opts), + getAllOwn: (effects: E) => + getServiceInterfaces(effects, {}), + getAll: ( + effects: E, + opts: { packageId: PackageId }, + ) => getServiceInterfaces(effects, opts), + }, + + store: { + get: ( + effects: E, + packageId: string, + path: PathBuilder, + ) => + getStore(effects, path, { + packageId, + }), + getOwn: ( + effects: E, + path: PathBuilder, + ) => getStore(effects, path), + setOwn: >( + effects: E, + path: Path, + value: Path extends PathBuilder ? Value : never, + ) => + effects.store.set({ + value, + path: extractJsonPath(path), + }), + }, + + MultiHost: { + of: (effects: Effects, id: string) => new MultiHost({ id, effects }), + }, + nullIfEmpty, + runCommand: async ( + effects: Effects, + image: { + imageId: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + }, + command: T.CommandType, + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + }, + name: string, + ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { + return runCommand(effects, image, command, options, name) + }, + /** + * @description Use this class to create an Action. By convention, each Action should receive its own file. + * + */ + Action: { + /** + * @description Use this function to create an action that accepts form input + * @param id - a unique ID for this action + * @param metadata - information describing the action and its availability + * @param inputSpec - define the form input using the InputSpec and Value classes + * @param prefillFn - optionally fetch data from the file system to pre-fill the input form. Must returns a deep partial of the input spec + * @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1" + * @example + * In this example, we create an action for a user to provide their name. + * We prefill the input form with their existing name from the service's yaml file. + * The new name is saved to the yaml file, and we return nothing to the user, which + * means they will receive a generic success message. + * + * ``` + import { sdk } from '../sdk' + import { yamlFile } from '../file-models/config.yml' + + const { InputSpec, Value } = sdk + + export const inputSpec = InputSpec.of({ + name: Value.text({ + name: 'Name', + description: + 'When you launch the Hello World UI, it will display "Hello [Name]"', + required: true, + default: 'World', + }), + }) + + export const setName = sdk.Action.withInput( + // id + 'set-name', + + // metadata + async ({ effects }) => ({ + name: 'Set Name', + description: 'Set your name so Hello World can say hello to you', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // form input specification + inputSpec, + + // optionally pre-fill the input form + async ({ effects }) => { + const name = await yamlFile.read.const(effects)?.name + return { name } + }, + + // the execution function + async ({ effects, input }) => yamlFile.merge(input) + ) + * ``` + */ + withInput: < + Id extends T.ActionId, + InputSpecType extends + | Record + | InputSpec + | InputSpec, + Type extends + ExtractInputSpecType = ExtractInputSpecType, + >( + id: Id, + metadata: MaybeFn>, + inputSpec: InputSpecType, + getInput: GetInput, + run: Run, + ) => Action.withInput(id, metadata, inputSpec, getInput, run), + /** + * @description Use this function to create an action that does not accept form input + * @param id - a unique ID for this action + * @param metadata - information describing the action and its availability + * @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1" + * @example + * In this example, we create an action that returns a secret phrase for the user to see. + * + * ``` + import { sdk } from '../sdk' + + export const showSecretPhrase = sdk.Action.withoutInput( + // id + 'show-secret-phrase', + + // metadata + async ({ effects }) => ({ + name: 'Show Secret Phrase', + description: 'Reveal the secret phrase for Hello World', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // the execution function + async ({ effects }) => ({ + version: '1', + title: 'Secret Phrase', + message: + 'Below is your secret phrase. Use it to gain access to extraordinary places', + result: { + type: 'single', + value: await sdk.store + .getOwn(effects, sdk.StorePath.secretPhrase) + .const(), + copyable: true, + qr: true, + masked: true, + }, + }), + ) + * ``` + */ + withoutInput: ( + id: Id, + metadata: MaybeFn>, + run: Run<{}>, + ) => Action.withoutInput(id, metadata, run), + }, + inputSpecConstants: { smtpInputSpec }, + /** + * @description Use this function to create a service interface. + * @param effects + * @param options + * @example + * In this example, we create a standard web UI + * + * ``` + const ui = sdk.createInterface(effects, { + name: 'Web UI', + id: 'ui', + description: 'The primary web app for this service.', + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + * ``` + */ + createInterface: ( + effects: Effects, + options: { + /** The human readable name of this service interface. */ + name: string + /** A unique ID for this service interface. */ + id: string + /** The human readable description. */ + description: string + /** Affects how the interface appears to the user. One of: 'ui', 'api', 'p2p'. If 'ui', the user will see a "Launch UI" button */ + type: ServiceInterfaceType + /** (optional) prepends the provided username to all URLs. */ + username: null | string + /** (optional) appends the provided path to all URLs. */ + path: string + /** (optional) appends the provided query params to all URLs. */ + search: Record + /** (optional) overrides the protocol prefix provided by the bind function. + * + * @example `ftp://` + */ + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + /** TODO Aiden how would someone include a password in the URL? Whether or not to mask the URLs on the screen, for example, when they contain a password */ + masked: boolean + }, + ) => new ServiceInterfaceBuilder({ ...options, effects }), + getSystemSmtp: (effects: E) => + new GetSystemSmtp(effects), + getSslCerificate: ( + effects: E, + hostnames: string[], + algorithm?: T.Algorithm, + ) => new GetSslCertificate(effects, hostnames, algorithm), + HealthCheck: { + of(effects: T.Effects, o: Omit) { + return healthCheck({ effects, ...o }) + }, + }, + healthCheck: { + checkPortListening, + checkWebUrl, + runHealthScript, + }, + patterns, + /** + * @description Use this function to list every Action offered by the service. Actions will be displayed in the provided order. + * + * By convention, each Action should receive its own file in the "actions" directory. + * @example + * + * ``` + import { sdk } from '../sdk' + import { config } from './config' + import { nameToLogs } from './nameToLogs' + + export const actions = sdk.Actions.of().addAction(config).addAction(nameToLogs) + * ``` + */ + Actions: Actions, + /** + * @description Use this function to determine which volumes are backed up when a user creates a backup, including advanced options. + * @example + * In this example, we back up the entire "main" volume and nothing else. + * + * ``` + import { sdk } from './sdk' + + export const { createBackup, restoreBackup } = sdk.setupBackups( + async ({ effects }) => sdk.Backups.volumes('main'), + ) + * ``` + * @example + * In this example, we back up the "main" volume, but exclude hypothetical directory "excludedDir". + * + * ``` + import { sdk } from './sdk' + + export const { createBackup, restoreBackup } = sdk.setupBackups(async () => + sdk.Backups.volumes('main').setOptions({ + exclude: ['excludedDir'], + }), + ) + * ``` + */ + setupBackups: (options: SetupBackupsParams) => + setupBackups(options), + /** + * @description Use this function to set dependency information. + * @example + * In this example, we create a perpetual dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check. + * + * ``` + export const setDependencies = sdk.setupDependencies( + async ({ effects, input }) => { + return { + 'hello-world': { + kind: 'running', + versionRange: '>=1.0.0', + healthChecks: ['primary'], + }, + } + }, + ) + * ``` + * @example + * In this example, we create a conditional dependency on Hello World based on a hypothetical "needsWorld" boolean in our Store. + * Using .const() ensures that if the "needsWorld" boolean changes, setupDependencies will re-run. + * + * ``` + export const setDependencies = sdk.setupDependencies( + async ({ effects }) => { + if (sdk.store.getOwn(sdk.StorePath.needsWorld).const()) { + return { + 'hello-world': { + kind: 'running', + versionRange: '>=1.0.0', + healthChecks: ['primary'], + }, + } + } + return {} + }, + ) + * ``` + */ + setupDependencies: setupDependencies, + setupInit: setupInit, + /** + * @description Use this function to execute arbitrary logic *once*, on initial install only. + * @example + * In the this example, we bootstrap our Store with a random, 16-char admin password. + * + * ``` + const install = sdk.setupInstall(async ({ effects }) => { + await sdk.store.setOwn( + effects, + sdk.StorePath.adminPassword, + utils.getDefaultString({ + charset: 'a-z,A-Z,1-9,!,@,$,%,&,', + len: 16, + }), + ) + }) + * ``` + */ + setupInstall: (fn: InstallFn) => Install.of(fn), + /** + * @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save. + * + * "input" will be of type `Input` for inputSpec save. It will be `null` for install and update. + * + * To learn about creating multi-hosts and interfaces, check out the {@link https://docs.start9.com/packaging-guide/learn/interfaces documentation}. + * @param inputSpec - The inputSpec spec of this service as exported from /inputSpec/spec. + * @param fn - an async function that returns an array of interface receipts. The function always has access to `effects`; it has access to `input` only after inputSpec save, otherwise `input` will be null. + * @example + * In this example, we create two UIs from one multi-host, and one API from another multi-host. + * + * ``` + export const setInterfaces = sdk.setupInterfaces( + inputSpecSpec, + async ({ effects, input }) => { + // ** UI multi-host ** + const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') + const uiMultiOrigin = await uiMulti.bindPort(80, { + protocol: 'http', + }) + // Primary UI + const primaryUi = sdk.createInterface(effects, { + name: 'Primary UI', + id: 'primary-ui', + description: 'The primary web app for this service.', + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + // Admin UI + const adminUi = sdk.createInterface(effects, { + name: 'Admin UI', + id: 'admin-ui', + description: 'The admin web app for this service.', + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '/admin', + search: {}, + }) + // UI receipt + const uiReceipt = await uiMultiOrigin.export([primaryUi, adminUi]) + + // ** API multi-host ** + const apiMulti = sdk.MultiHost.of(effects, 'api-multi') + const apiMultiOrigin = await apiMulti.bindPort(5959, { + protocol: 'http', + }) + // API + const api = sdk.createInterface(effects, { + name: 'Admin API', + id: 'api', + description: 'The advanced API for this service.', + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + // API receipt + const apiReceipt = await apiMultiOrigin.export([api]) + + // ** Return receipts ** + return [uiReceipt, apiReceipt] + }, + ) + * ``` + */ + setupInterfaces: setupServiceInterfaces, + setupMain: ( + fn: (o: { + effects: Effects + started(onTerm: () => PromiseLike): PromiseLike + }) => Promise>, + ) => setupMain(fn), + /** + * Use this function to execute arbitrary logic *once*, on uninstall only. Most services will not use this. + */ + setupUninstall: (fn: UninstallFn) => + setupUninstall(fn), + trigger: { + defaultTrigger, + cooldownTrigger, + changeOnFirstSuccess, + successFailure, + }, + Mounts: { + of() { + return Mounts.of() + }, + }, + Backups: { + volumes: ( + ...volumeNames: Array + ) => Backups.withVolumes(...volumeNames), + addSets: ( + ...options: BackupSync[] + ) => Backups.withSyncs(...options), + withOptions: (options?: Partial) => + Backups.withOptions(options), + }, + InputSpec: { + /** + * @description Use this function to define the inputSpec specification that will ultimately present to the user as validated form inputs. + * + * Most form controls are supported, including text, textarea, number, toggle, select, multiselect, list, color, datetime, object (sub form), and union (conditional sub form). + * @example + * In this example, we define a inputSpec form with two value: name and makePublic. + * + * ``` + import { sdk } from '../sdk' + const { InputSpec, Value } = sdk + + export const inputSpecSpec = InputSpec.of({ + name: Value.text({ + name: 'Name', + description: + 'When you launch the Hello World UI, it will display "Hello [Name]"', + required: true, + default: 'World' + }), + makePublic: Value.toggle({ + name: 'Make Public', + description: 'Whether or not to expose the service to the network', + default: false, + }), + }) + * ``` + */ + of: < + Spec extends Record | Value>, + >( + spec: Spec, + ) => InputSpec.of(spec), + }, + Daemons: { + of( + effects: Effects, + started: (onTerm: () => PromiseLike) => PromiseLike, + healthReceipts: HealthReceipt[], + ) { + return Daemons.of({ effects, started, healthReceipts }) + }, + }, + SubContainer: { + of( + effects: Effects, + image: { + imageId: T.ImageId & keyof Manifest["images"] + sharedRun?: boolean + }, + name: string, + ) { + return SubContainer.of(effects, image, name) + }, + }, + List: { + /** + * @description Create a list of text inputs. + * @param a - attributes of the list itself. + * @param aSpec - attributes describing each member of the list. + */ + text: List.text, + /** + * @description Create a list of objects. + * @param a - attributes of the list itself. + * @param aSpec - attributes describing each member of the list. + */ + obj: >( + a: { + name: string + description?: string | null + /** Presents a warning before adding/removing/editing a list item. */ + warning?: string | null + default?: [] + minLength?: number | null + maxLength?: number | null + }, + aSpec: { + spec: InputSpec + /** + * @description The ID of a required field on the inner object whose value will be used to display items in the list. + * @example + * In this example, we use the value of the `label` field to display members of the list. + * + * ``` + spec: InputSpec.of({ + label: Value.text({ + name: 'Label', + required: false, + default: null, + }) + }) + displayAs: 'label', + uniqueBy: null, + * ``` + * + */ + displayAs?: null | string + /** + * @description The ID(s) of required fields on the inner object whose value(s) will be used to enforce uniqueness in the list. + * @example + * In this example, we use the `label` field to enforce uniqueness, meaning the label field must be unique from other entries. + * + * ``` + spec: InputSpec.of({ + label: Value.text({ + name: 'Label', + required: true, + default: null, + }) + pubkey: Value.text({ + name: 'Pubkey', + required: true, + default: null, + }) + }) + displayAs: 'label', + uniqueBy: 'label', + * ``` + * @example + * In this example, we use the `label` field AND the `pubkey` field to enforce uniqueness, meaning both these fields must be unique from other entries. + * + * ``` + spec: InputSpec.of({ + label: Value.text({ + name: 'Label', + required: true, + default: null, + }) + pubkey: Value.text({ + name: 'Pubkey', + required: true, + default: null, + }) + }) + displayAs: 'label', + uniqueBy: { all: ['label', 'pubkey'] }, + * ``` + */ + uniqueBy?: null | UniqueBy + }, + ) => List.obj(a, aSpec), + /** + * @description Create a list of dynamic text inputs. + * @param a - attributes of the list itself. + * @param aSpec - attributes describing each member of the list. + */ + dynamicText: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + warning?: string | null + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + generate?: null | RandomString + spec: { + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns: Pattern[] + inputmode?: ListValueSpecText["inputmode"] + } + } + >, + ) => List.dynamicText(getA), + }, + StorePath: pathBuilder(), + Value: { + /** + * @description Displays a boolean toggle to enable/disable + * @example + * ``` + toggleExample: Value.toggle({ + // required + name: 'Toggle Example', + default: true, + + // optional + description: null, + warning: null, + immutable: false, + }), + * ``` + */ + toggle: Value.toggle, + /** + * @description Displays a text input field + * @example + * ``` + textExample: Value.text({ + // required + name: 'Text Example', + required: false, + default: null, + + // optional + description: null, + placeholder: null, + warning: null, + generate: null, + inputmode: 'text', + masked: false, + minLength: null, + maxLength: null, + patterns: [], + immutable: false, + }), + * ``` + */ + text: Value.text, + /** + * @description Displays a large textarea field for long form entry. + * @example + * ``` + textareaExample: Value.textarea({ + // required + name: 'Textarea Example', + required: false, + default: null, + + // optional + description: null, + placeholder: null, + warning: null, + minLength: null, + maxLength: null, + immutable: false, + }), + * ``` + */ + textarea: Value.textarea, + /** + * @description Displays a number input field + * @example + * ``` + numberExample: Value.number({ + // required + name: 'Number Example', + required: false, + default: null, + integer: true, + + // optional + description: null, + placeholder: null, + warning: null, + min: null, + max: null, + immutable: false, + step: null, + units: null, + }), + * ``` + */ + number: Value.number, + /** + * @description Displays a browser-native color selector. + * @example + * ``` + colorExample: Value.color({ + // required + name: 'Color Example', + required: false, + default: null, + + // optional + description: null, + warning: null, + immutable: false, + }), + * ``` + */ + color: Value.color, + /** + * @description Displays a browser-native date/time selector. + * @example + * ``` + datetimeExample: Value.datetime({ + // required + name: 'Datetime Example', + required: false, + default: null, + + // optional + description: null, + warning: null, + immutable: false, + inputmode: 'datetime-local', + min: null, + max: null, + }), + * ``` + */ + datetime: Value.datetime, + /** + * @description Displays a select modal with radio buttons, allowing for a single selection. + * @example + * ``` + selectExample: Value.select({ + // required + name: 'Select Example', + default: 'radio1', + values: { + radio1: 'Radio 1', + radio2: 'Radio 2', + }, + + // optional + description: null, + warning: null, + immutable: false, + disabled: false, + }), + * ``` + */ + select: Value.select, + /** + * @description Displays a select modal with checkboxes, allowing for multiple selections. + * @example + * ``` + multiselectExample: Value.multiselect({ + // required + name: 'Multiselect Example', + values: { + option1: 'Option 1', + option2: 'Option 2', + }, + default: [], + + // optional + description: null, + warning: null, + immutable: false, + disabled: false, + minlength: null, + maxLength: null, + }), + * ``` + */ + multiselect: Value.multiselect, + /** + * @description Display a collapsable grouping of additional fields, a "sub form". The second value is the inputSpec spec for the sub form. + * @example + * ``` + objectExample: Value.object( + { + // required + name: 'Object Example', + + // optional + description: null, + warning: null, + }, + InputSpec.of({}), + ), + * ``` + */ + object: Value.object, + /** + * @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented. + * @example + * ``` + unionExample: Value.union( + { + // required + name: 'Union Example', + default: 'option1', + + // optional + description: null, + warning: null, + disabled: false, + immutable: false, + }, + Variants.of({ + option1: { + name: 'Option 1', + spec: InputSpec.of({}), + }, + option2: { + name: 'Option 2', + spec: InputSpec.of({}), + }, + }), + ), + * ``` + */ + union: Value.union, + /** + * @description Presents an interface to add/remove/edit items in a list. + * @example + * In this example, we create a list of text inputs. + * + * ``` + listExampleText: Value.list( + List.text( + { + // required + name: 'Text List', + + // optional + description: null, + warning: null, + default: [], + minLength: null, + maxLength: null, + }, + { + // required + patterns: [], + + // optional + placeholder: null, + generate: null, + inputmode: 'url', + masked: false, + minLength: null, + maxLength: null, + }, + ), + ), + * ``` + * @example + * In this example, we create a list of objects. + * + * ``` + listExampleObject: Value.list( + List.obj( + { + // required + name: 'Object List', + + // optional + description: null, + warning: null, + default: [], + minLength: null, + maxLength: null, + }, + { + // required + spec: InputSpec.of({}), + + // optional + displayAs: null, + uniqueBy: null, + }, + ), + ), + * ``` + */ + list: Value.list, + hidden: Value.hidden, + dynamicToggle: ( + a: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + default: boolean + disabled?: false | string + } + >, + ) => Value.dynamicToggle(a), + dynamicText: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { string | RandomString | null } + * @example default: null + * @example default: 'World' + * @example default: { charset: 'abcdefg', len: 16 } + */ + default: DefaultString | null + required: boolean + /** + * @description Mask (aka camouflage) text input with dots: ● ● ● + * @default false + */ + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + /** + * @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails. + * @default [] + * @example + * ``` + [ + { + regex: "[a-z]", + description: "May only contain lower case letters from the English alphabet." + } + ] + * ``` + */ + patterns?: Pattern[] + /** + * @description Informs the browser how to behave and which keyboard to display on mobile + * @default "text" + */ + inputmode?: ValueSpecText["inputmode"] + /** + * @description Displays a button that will generate a random string according to the provided charset and len attributes. + */ + generate?: null | RandomString + } + >, + ) => Value.dynamicText(getA), + dynamicTextarea: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + default: string | null + required: boolean + minLength?: number | null + maxLength?: number | null + placeholder?: string | null + disabled?: false | string + } + >, + ) => Value.dynamicTextarea(getA), + dynamicNumber: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { number | null } + * @example default: null + * @example default: 7 + */ + default: number | null + required: boolean + min?: number | null + max?: number | null + /** + * @description How much does the number increase/decrease when using the arrows provided by the browser. + * @default 1 + */ + step?: number | null + /** + * @description Requires the number to be an integer. + */ + integer: boolean + /** + * @description Optionally display units to the right of the input box. + */ + units?: string | null + placeholder?: string | null + disabled?: false | string + } + >, + ) => Value.dynamicNumber(getA), + dynamicColor: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { string | null } + * @example default: null + * @example default: 'ffffff' + */ + default: string | null + required: boolean + disabled?: false | string + } + >, + ) => Value.dynamicColor(getA), + dynamicDatetime: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description optionally provide a default value. + * @type { string | null } + * @example default: null + * @example default: '1985-12-16 18:00:00.000' + */ + default: string + required: boolean + /** + * @description Informs the browser how to behave and which date/time component to display. + * @default "datetime-local" + */ + inputmode?: ValueSpecDatetime["inputmode"] + min?: string | null + max?: string | null + disabled?: false | string + } + >, + ) => Value.dynamicDatetime(getA), + dynamicSelect: >( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description provide a default value from the list of values. + * @type { default: string } + * @example default: 'radio1' + */ + default: keyof Variants & string + /** + * @description A mapping of unique radio options to their human readable display format. + * @example + * ``` + { + radio1: "Radio 1" + radio2: "Radio 2" + radio3: "Radio 3" + } + * ``` + */ + values: Variants + /** + * @options + * - false - The field can be modified. + * - string - The field cannot be modified. The provided text explains why. + * - string[] - The field can be modified, but the values contained in the array cannot be selected. + * @default false + */ + disabled?: false | string | string[] + } + >, + ) => Value.dynamicSelect(getA), + dynamicMultiselect: ( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description A simple list of which options should be checked by default. + */ + default: string[] + /** + * @description A mapping of checkbox options to their human readable display format. + * @example + * ``` + { + option1: "Option 1" + option2: "Option 2" + option3: "Option 3" + } + * ``` + */ + values: Record + minLength?: number | null + maxLength?: number | null + /** + * @options + * - false - The field can be modified. + * - string - The field cannot be modified. The provided text explains why. + * - string[] - The field can be modified, but the values contained in the array cannot be selected. + * @default false + */ + disabled?: false | string | string[] + } + >, + ) => Value.dynamicMultiselect(getA), + filteredUnion: < + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + >( + getDisabledFn: LazyBuild, + a: { + name: string + description?: string | null + warning?: string | null + default: keyof VariantValues & string + }, + aVariants: + | Variants + | Variants, + ) => + Value.filteredUnion( + getDisabledFn, + a, + aVariants, + ), + + dynamicUnion: < + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec | InputSpec + } + }, + >( + getA: LazyBuild< + Store, + { + name: string + description?: string | null + /** Presents a warning prompt before permitting the value to change. */ + warning?: string | null + /** + * @description provide a default value from the list of variants. + * @type { string } + * @example default: 'variant1' + */ + default: keyof VariantValues & string + required: boolean + /** + * @options + * - false - The field can be modified. + * - string - The field cannot be modified. The provided text explains why. + * - string[] - The field can be modified, but the values contained in the array cannot be selected. + * @default false + */ + disabled: false | string | string[] + } + >, + aVariants: + | Variants + | Variants, + ) => Value.dynamicUnion(getA, aVariants), + }, + Variants: { + of: < + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec + } + }, + >( + a: VariantValues, + ) => Variants.of(a), + }, + } + } +} + +export async function runCommand( + effects: Effects, + image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, + command: string | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + }, + name: string, +): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { + const commands = splitCommand(command) + return SubContainer.with( + effects, + image, + options.mounts || [], + name, + (subcontainer) => subcontainer.exec(commands), + ) +} diff --git a/sdk/package/lib/backup/Backups.ts b/sdk/package/lib/backup/Backups.ts new file mode 100644 index 000000000..c7c6301f5 --- /dev/null +++ b/sdk/package/lib/backup/Backups.ts @@ -0,0 +1,187 @@ +import * as T from "../../../base/lib/types" +import * as child_process from "child_process" +import { asError } from "../util" + +export const DEFAULT_OPTIONS: T.SyncOptions = { + delete: true, + exclude: [], +} +export type BackupSync = { + dataPath: `/media/startos/volumes/${Volumes}/${string}` + backupPath: `/media/startos/backup/${string}` + options?: Partial + backupOptions?: Partial + restoreOptions?: Partial +} + +export class Backups { + private constructor( + private options = DEFAULT_OPTIONS, + private restoreOptions: Partial = {}, + private backupOptions: Partial = {}, + private backupSet = [] as BackupSync[], + ) {} + + static withVolumes( + ...volumeNames: Array + ): Backups { + return Backups.withSyncs( + ...volumeNames.map((srcVolume) => ({ + dataPath: `/media/startos/volumes/${srcVolume}/` as const, + backupPath: `/media/startos/backup/${srcVolume}/` as const, + })), + ) + } + + static withSyncs( + ...syncs: BackupSync[] + ) { + return syncs.reduce((acc, x) => acc.addSync(x), new Backups()) + } + + static withOptions( + options?: Partial, + ) { + return new Backups({ ...DEFAULT_OPTIONS, ...options }) + } + + setOptions(options?: Partial) { + this.options = { + ...this.options, + ...options, + } + return this + } + + setBackupOptions(options?: Partial) { + this.backupOptions = { + ...this.backupOptions, + ...options, + } + return this + } + + setRestoreOptions(options?: Partial) { + this.restoreOptions = { + ...this.restoreOptions, + ...options, + } + return this + } + + addVolume( + volume: M["volumes"][number], + options?: Partial<{ + options: T.SyncOptions + backupOptions: T.SyncOptions + restoreOptions: T.SyncOptions + }>, + ) { + return this.addSync({ + dataPath: `/media/startos/volumes/${volume}/` as const, + backupPath: `/media/startos/backup/${volume}/` as const, + ...options, + }) + } + addSync(sync: BackupSync) { + this.backupSet.push({ + ...sync, + options: { ...this.options, ...sync.options }, + }) + return this + } + + async createBackup() { + for (const item of this.backupSet) { + const rsyncResults = await runRsync({ + srcPath: item.dataPath, + dstPath: item.backupPath, + options: { + ...this.options, + ...this.backupOptions, + ...item.options, + ...item.backupOptions, + }, + }) + await rsyncResults.wait() + } + return + } + + async restoreBackup() { + for (const item of this.backupSet) { + const rsyncResults = await runRsync({ + srcPath: item.backupPath, + dstPath: item.dataPath, + options: { + ...this.options, + ...this.backupOptions, + ...item.options, + ...item.backupOptions, + }, + }) + await rsyncResults.wait() + } + return + } +} + +async function runRsync(rsyncOptions: { + srcPath: string + dstPath: string + options: T.SyncOptions +}): Promise<{ + id: () => Promise + wait: () => Promise + progress: () => Promise +}> { + const { srcPath, dstPath, options } = rsyncOptions + + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(srcPath) + args.push(dstPath) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(`Backups.runAsync`, asError(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code: any) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } +} diff --git a/sdk/package/lib/backup/index.ts b/sdk/package/lib/backup/index.ts new file mode 100644 index 000000000..1e7995252 --- /dev/null +++ b/sdk/package/lib/backup/index.ts @@ -0,0 +1,2 @@ +import "./Backups" +import "./setupBackups" diff --git a/sdk/package/lib/backup/setupBackups.ts b/sdk/package/lib/backup/setupBackups.ts new file mode 100644 index 000000000..722d245ff --- /dev/null +++ b/sdk/package/lib/backup/setupBackups.ts @@ -0,0 +1,39 @@ +import { Backups } from "./Backups" +import * as T from "../../../base/lib/types" +import { _ } from "../util" + +export type SetupBackupsParams = + | M["volumes"][number][] + | ((_: { effects: T.Effects }) => Promise>) + +type SetupBackupsRes = { + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup +} + +export function setupBackups( + options: SetupBackupsParams, +) { + let backupsFactory: (_: { effects: T.Effects }) => Promise> + if (options instanceof Function) { + backupsFactory = options + } else { + backupsFactory = async () => Backups.withVolumes(...options) + } + const answer: { + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup + } = { + get createBackup() { + return (async (options) => { + return (await backupsFactory(options)).createBackup() + }) as T.ExpectedExports.createBackup + }, + get restoreBackup() { + return (async (options) => { + return (await backupsFactory(options)).restoreBackup() + }) as T.ExpectedExports.restoreBackup + }, + } + return answer +} diff --git a/sdk/package/lib/health/HealthCheck.ts b/sdk/package/lib/health/HealthCheck.ts new file mode 100644 index 000000000..05c1a214a --- /dev/null +++ b/sdk/package/lib/health/HealthCheck.ts @@ -0,0 +1,64 @@ +import { Effects, HealthReceipt } from "../../../base/lib/types" +import { HealthCheckResult } from "./checkFns/HealthCheckResult" +import { Trigger } from "../trigger" +import { TriggerInput } from "../trigger/TriggerInput" +import { defaultTrigger } from "../trigger/defaultTrigger" +import { once, asError } from "../util" +import { object, unknown } from "ts-matches" + +export type HealthCheckParams = { + effects: Effects + name: string + trigger?: Trigger + fn(): Promise | HealthCheckResult + onFirstSuccess?: () => unknown | Promise +} + +export function healthCheck(o: HealthCheckParams) { + new Promise(async () => { + let currentValue: TriggerInput = {} + const getCurrentValue = () => currentValue + const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) + const triggerFirstSuccess = once(() => + Promise.resolve( + "onFirstSuccess" in o && o.onFirstSuccess + ? o.onFirstSuccess() + : undefined, + ), + ) + for ( + let res = await trigger.next(); + !res.done; + res = await trigger.next() + ) { + try { + const { result, message } = await o.fn() + await o.effects.setHealth({ + name: o.name, + id: o.name, + result, + message: message || "", + }) + currentValue.lastResult = result + await triggerFirstSuccess().catch((err) => { + console.error(asError(err)) + }) + } catch (e) { + await o.effects.setHealth({ + name: o.name, + id: o.name, + result: "failure", + message: asMessage(e) || "", + }) + currentValue.lastResult = "failure" + } + } + }) + return {} as HealthReceipt +} +function asMessage(e: unknown) { + if (object({ message: unknown }).test(e)) return String(e.message) + const value = String(e) + if (value.length == null) return null + return value +} diff --git a/sdk/package/lib/health/checkFns/HealthCheckResult.ts b/sdk/package/lib/health/checkFns/HealthCheckResult.ts new file mode 100644 index 000000000..92d4afddf --- /dev/null +++ b/sdk/package/lib/health/checkFns/HealthCheckResult.ts @@ -0,0 +1,3 @@ +import { T } from "../../../../base/lib" + +export type HealthCheckResult = Omit diff --git a/sdk/package/lib/health/checkFns/checkPortListening.ts b/sdk/package/lib/health/checkFns/checkPortListening.ts new file mode 100644 index 000000000..59cd9717f --- /dev/null +++ b/sdk/package/lib/health/checkFns/checkPortListening.ts @@ -0,0 +1,77 @@ +import { Effects } from "../../../../base/lib/types" +import { stringFromStdErrOut } from "../../util" +import { HealthCheckResult } from "./HealthCheckResult" +import { promisify } from "node:util" +import * as CP from "node:child_process" + +const cpExec = promisify(CP.exec) + +export function containsAddress(x: string, port: number, address?: bigint) { + const readPorts = x + .split("\n") + .filter(Boolean) + .splice(1) + .map((x) => x.split(" ").filter(Boolean)[1]?.split(":")) + .filter((x) => x?.length > 1) + .map(([addr, p]) => [BigInt(`0x${addr}`), Number.parseInt(p, 16)] as const) + return !!readPorts.find( + ([addr, p]) => (address === undefined || address === addr) && port === p, + ) +} + +/** + * This is used to check if a port is listening on the system. + * Used during the health check fn or the check main fn. + */ +export async function checkPortListening( + effects: Effects, + port: number, + options: { + errorMessage: string + successMessage: string + timeoutMessage?: string + timeout?: number + }, +): Promise { + return Promise.race([ + Promise.resolve().then(async () => { + const hasAddress = + containsAddress( + await cpExec(`cat /proc/net/tcp`, {}).then(stringFromStdErrOut), + port, + ) || + containsAddress( + await cpExec(`cat /proc/net/tcp6`, {}).then(stringFromStdErrOut), + port, + BigInt(0), + ) || + containsAddress( + await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut), + port, + ) || + containsAddress( + await cpExec("cat /proc/net/udp6", {}).then(stringFromStdErrOut), + port, + BigInt(0), + ) + if (hasAddress) { + return { result: "success", message: options.successMessage } + } + return { + result: "failure", + message: options.errorMessage, + } + }), + new Promise((resolve) => { + setTimeout( + () => + resolve({ + result: "failure", + message: + options.timeoutMessage || `Timeout trying to check port ${port}`, + }), + options.timeout ?? 1_000, + ) + }), + ]) +} diff --git a/sdk/package/lib/health/checkFns/checkWebUrl.ts b/sdk/package/lib/health/checkFns/checkWebUrl.ts new file mode 100644 index 000000000..e04ee7531 --- /dev/null +++ b/sdk/package/lib/health/checkFns/checkWebUrl.ts @@ -0,0 +1,36 @@ +import { Effects } from "../../../../base/lib/types" +import { asError } from "../../util" +import { HealthCheckResult } from "./HealthCheckResult" +import { timeoutPromise } from "./index" +import "isomorphic-fetch" + +/** + * This is a helper function to check if a web url is reachable. + * @param url + * @param createSuccess + * @returns + */ +export const checkWebUrl = async ( + effects: Effects, + url: string, + { + timeout = 1000, + successMessage = `Reached ${url}`, + errorMessage = `Error while fetching URL: ${url}`, + } = {}, +): Promise => { + return Promise.race([fetch(url), timeoutPromise(timeout)]) + .then( + (x) => + ({ + result: "success", + message: successMessage, + }) as const, + ) + .catch((e) => { + console.warn(`Error while fetching URL: ${url}`) + console.error(JSON.stringify(e)) + console.error(asError(e)) + return { result: "failure" as const, message: errorMessage } + }) +} diff --git a/sdk/package/lib/health/checkFns/index.ts b/sdk/package/lib/health/checkFns/index.ts new file mode 100644 index 000000000..2de37e38c --- /dev/null +++ b/sdk/package/lib/health/checkFns/index.ts @@ -0,0 +1,11 @@ +import { runHealthScript } from "./runHealthScript" +export { checkPortListening } from "./checkPortListening" +export { HealthCheckResult } from "./HealthCheckResult" +export { checkWebUrl } from "./checkWebUrl" + +export function timeoutPromise(ms: number, { message = "Timed out" } = {}) { + return new Promise((resolve, reject) => + setTimeout(() => reject(new Error(message)), ms), + ) +} +export { runHealthScript } diff --git a/sdk/package/lib/health/checkFns/runHealthScript.ts b/sdk/package/lib/health/checkFns/runHealthScript.ts new file mode 100644 index 000000000..7ecd1ad75 --- /dev/null +++ b/sdk/package/lib/health/checkFns/runHealthScript.ts @@ -0,0 +1,35 @@ +import { HealthCheckResult } from "./HealthCheckResult" +import { timeoutPromise } from "./index" +import { SubContainer } from "../../util/SubContainer" + +/** + * Running a health script, is used when we want to have a simple + * script in bash or something like that. It should return something that is useful + * in {result: string} else it is considered an error + * @param param0 + * @returns + */ +export const runHealthScript = async ( + runCommand: string[], + subcontainer: SubContainer, + { + timeout = 30000, + errorMessage = `Error while running command: ${runCommand}`, + message = (res: string) => + `Have ran script ${runCommand} and the result: ${res}`, + } = {}, +): Promise => { + const res = await Promise.race([ + subcontainer.exec(runCommand), + timeoutPromise(timeout), + ]).catch((e) => { + console.warn(errorMessage) + console.warn(JSON.stringify(e)) + console.warn(e.toString()) + throw { result: "failure", message: errorMessage } as HealthCheckResult + }) + return { + result: "success", + message: message(res.stdout.toString()), + } as HealthCheckResult +} diff --git a/sdk/package/lib/health/index.ts b/sdk/package/lib/health/index.ts new file mode 100644 index 000000000..b969037a5 --- /dev/null +++ b/sdk/package/lib/health/index.ts @@ -0,0 +1 @@ +import "./checkFns" diff --git a/sdk/package/lib/index.ts b/sdk/package/lib/index.ts new file mode 100644 index 000000000..3619765e1 --- /dev/null +++ b/sdk/package/lib/index.ts @@ -0,0 +1,48 @@ +import { + S9pk, + Version, + VersionRange, + ExtendedVersion, + inputSpec, + ISB, + IST, + types, + T, + matches, + utils, +} from "../../base/lib" + +export { + S9pk, + Version, + VersionRange, + ExtendedVersion, + inputSpec, + ISB, + IST, + types, + T, + matches, + utils, +} +export { Daemons } from "./mainFn/Daemons" +export { SubContainer } from "./util/SubContainer" +export { StartSdk } from "./StartSdk" +export { setupManifest, buildManifest } from "./manifest/setupManifest" +export { FileHelper } from "./util/fileHelper" +export { setupExposeStore } from "./store/setupExposeStore" +export { pathBuilder } from "../../base/lib/util/PathBuilder" + +export * as actions from "../../base/lib/actions" +export * as backup from "./backup" +export * as daemons from "./mainFn/Daemons" +export * as health from "./health" +export * as healthFns from "./health/checkFns" +export * as inits from "./inits" +export * as mainFn from "./mainFn" +export * as toml from "@iarna/toml" +export * as yaml from "yaml" +export * as startSdk from "./StartSdk" +export * as YAML from "yaml" +export * as TOML from "@iarna/toml" +export * from "./version" diff --git a/sdk/package/lib/inits/index.ts b/sdk/package/lib/inits/index.ts new file mode 100644 index 000000000..0a326a61e --- /dev/null +++ b/sdk/package/lib/inits/index.ts @@ -0,0 +1,3 @@ +import "./setupInit" +import "./setupUninstall" +import "./setupInstall" diff --git a/sdk/package/lib/inits/setupInit.ts b/sdk/package/lib/inits/setupInit.ts new file mode 100644 index 000000000..8d30f5b1f --- /dev/null +++ b/sdk/package/lib/inits/setupInit.ts @@ -0,0 +1,64 @@ +import { Actions } from "../../../base/lib/actions/setupActions" +import { ExtendedVersion } from "../../../base/lib/exver" +import { UpdateServiceInterfaces } from "../../../base/lib/interfaces/setupInterfaces" +import { ExposedStorePaths } from "../../../base/lib/types" +import * as T from "../../../base/lib/types" +import { VersionGraph } from "../version/VersionGraph" +import { Install } from "./setupInstall" +import { Uninstall } from "./setupUninstall" + +export function setupInit( + versions: VersionGraph, + install: Install, + uninstall: Uninstall, + setServiceInterfaces: UpdateServiceInterfaces, + setDependencies: (options: { + effects: T.Effects + }) => Promise, + actions: Actions, + exposedStore: ExposedStorePaths, +): { + packageInit: T.ExpectedExports.packageInit + packageUninit: T.ExpectedExports.packageUninit + containerInit: T.ExpectedExports.containerInit +} { + return { + packageInit: async (opts) => { + const prev = await opts.effects.getDataVersion() + if (prev) { + await versions.migrate({ + effects: opts.effects, + from: ExtendedVersion.parse(prev), + to: versions.currentVersion(), + }) + } else { + await install.install(opts) + await opts.effects.setDataVersion({ + version: versions.current.options.version, + }) + } + }, + packageUninit: async (opts) => { + if (opts.nextVersion) { + const prev = await opts.effects.getDataVersion() + if (prev) { + await versions.migrate({ + effects: opts.effects, + from: ExtendedVersion.parse(prev), + to: ExtendedVersion.parse(opts.nextVersion), + }) + } + } else { + await uninstall.uninstall(opts) + } + }, + containerInit: async (opts) => { + await setServiceInterfaces({ + ...opts, + }) + await actions.update({ effects: opts.effects }) + await opts.effects.exposeForDependents({ paths: exposedStore }) + await setDependencies({ effects: opts.effects }) + }, + } +} diff --git a/sdk/package/lib/inits/setupInstall.ts b/sdk/package/lib/inits/setupInstall.ts new file mode 100644 index 000000000..38a96a00b --- /dev/null +++ b/sdk/package/lib/inits/setupInstall.ts @@ -0,0 +1,25 @@ +import * as T from "../../../base/lib/types" + +export type InstallFn = (opts: { + effects: T.Effects +}) => Promise +export class Install { + private constructor(readonly fn: InstallFn) {} + static of( + fn: InstallFn, + ) { + return new Install(fn) + } + + async install({ effects }: Parameters[0]) { + await this.fn({ + effects, + }) + } +} + +export function setupInstall( + fn: InstallFn, +) { + return Install.of(fn) +} diff --git a/sdk/package/lib/inits/setupUninstall.ts b/sdk/package/lib/inits/setupUninstall.ts new file mode 100644 index 000000000..fc4a71b8e --- /dev/null +++ b/sdk/package/lib/inits/setupUninstall.ts @@ -0,0 +1,29 @@ +import * as T from "../../../base/lib/types" + +export type UninstallFn = (opts: { + effects: T.Effects +}) => Promise +export class Uninstall { + private constructor(readonly fn: UninstallFn) {} + static of( + fn: UninstallFn, + ) { + return new Uninstall(fn) + } + + async uninstall({ + effects, + nextVersion, + }: Parameters[0]) { + if (!nextVersion) + await this.fn({ + effects, + }) + } +} + +export function setupUninstall( + fn: UninstallFn, +) { + return Uninstall.of(fn) +} diff --git a/sdk/package/lib/mainFn/CommandController.ts b/sdk/package/lib/mainFn/CommandController.ts new file mode 100644 index 000000000..a7375b369 --- /dev/null +++ b/sdk/package/lib/mainFn/CommandController.ts @@ -0,0 +1,155 @@ +import { DEFAULT_SIGTERM_TIMEOUT } from "." +import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types" + +import * as T from "../../../base/lib/types" +import { + MountOptions, + SubContainerHandle, + SubContainer, +} from "../util/SubContainer" +import { splitCommand } from "../util" +import * as cp from "child_process" + +export class CommandController { + private constructor( + readonly runningAnswer: Promise, + private state: { exited: boolean }, + private readonly subcontainer: SubContainer, + private process: cp.ChildProcess, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, + ) {} + static of() { + return async ( + effects: T.Effects, + subcontainer: + | { + imageId: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + } + | SubContainer, + command: T.CommandType, + options: { + subcontainerName?: string + // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms + sigtermTimeout?: number + mounts?: { path: string; options: MountOptions }[] + runAsInit?: boolean + env?: + | { + [variable: string]: string + } + | undefined + cwd?: string | undefined + user?: string | undefined + onStdout?: (chunk: Buffer | string | any) => void + onStderr?: (chunk: Buffer | string | any) => void + }, + ) => { + const commands = splitCommand(command) + const subc = + subcontainer instanceof SubContainer + ? subcontainer + : await (async () => { + const subc = await SubContainer.of( + effects, + subcontainer, + options?.subcontainerName || commands.join(" "), + ) + for (let mount of options.mounts || []) { + await subc.mount(mount.options, mount.path) + } + return subc + })() + + try { + let childProcess: cp.ChildProcess + if (options.runAsInit) { + childProcess = await subc.launch(commands, { + env: options.env, + }) + } else { + childProcess = await subc.spawn(commands, { + env: options.env, + stdio: options.onStdout || options.onStderr ? "pipe" : "inherit", + }) + } + + if (options.onStdout) childProcess.stdout?.on("data", options.onStdout) + if (options.onStderr) childProcess.stderr?.on("data", options.onStderr) + + const state = { exited: false } + const answer = new Promise((resolve, reject) => { + childProcess.on("exit", (code) => { + state.exited = true + if ( + code === 0 || + code === 143 || + (code === null && childProcess.signalCode == "SIGTERM") + ) { + return resolve(null) + } + if (code) { + return reject( + new Error(`${commands[0]} exited with code ${code}`), + ) + } else { + return reject( + new Error( + `${commands[0]} exited with signal ${childProcess.signalCode}`, + ), + ) + } + }) + }) + + return new CommandController( + answer, + state, + subc, + childProcess, + options.sigtermTimeout, + ) + } catch (e) { + await subc.destroy() + throw e + } + } + } + get subContainerHandle() { + return new SubContainerHandle(this.subcontainer) + } + async wait({ timeout = NO_TIMEOUT } = {}) { + if (timeout > 0) + setTimeout(() => { + this.term() + }, timeout) + try { + return await this.runningAnswer + } finally { + if (!this.state.exited) { + this.process.kill("SIGKILL") + } + await this.subcontainer.destroy().catch((_) => {}) + } + } + async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { + try { + if (!this.state.exited) { + if (signal !== "SIGKILL") { + setTimeout(() => { + if (!this.state.exited) this.process.kill("SIGKILL") + }, timeout) + } + if (!this.process.kill(signal)) { + console.error( + `failed to send signal ${signal} to pid ${this.process.pid}`, + ) + } + } + + await this.runningAnswer + } finally { + await this.subcontainer.destroy() + } + } +} diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts new file mode 100644 index 000000000..864ae4122 --- /dev/null +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -0,0 +1,91 @@ +import * as T from "../../../base/lib/types" +import { asError } from "../../../base/lib/util/asError" +import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer" +import { CommandController } from "./CommandController" + +const TIMEOUT_INCREMENT_MS = 1000 +const MAX_TIMEOUT_MS = 30000 +/** + * This is a wrapper around CommandController that has a state of off, where the command shouldn't be running + * and the others state of running, where it will keep a living running command + */ + +export class Daemon { + private commandController: CommandController | null = null + private shouldBeRunning = false + constructor(private startCommand: () => Promise) {} + get subContainerHandle(): undefined | ExecSpawnable { + return this.commandController?.subContainerHandle + } + static of() { + return async ( + effects: T.Effects, + subcontainer: + | { + imageId: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + } + | SubContainer, + command: T.CommandType, + options: { + subcontainerName?: string + mounts?: { path: string; options: MountOptions }[] + env?: + | { + [variable: string]: string + } + | undefined + cwd?: string | undefined + user?: string | undefined + onStdout?: (chunk: Buffer | string | any) => void + onStderr?: (chunk: Buffer | string | any) => void + sigtermTimeout?: number + }, + ) => { + const startCommand = () => + CommandController.of()( + effects, + subcontainer, + command, + options, + ) + return new Daemon(startCommand) + } + } + async start() { + if (this.commandController) { + return + } + this.shouldBeRunning = true + let timeoutCounter = 0 + new Promise(async () => { + while (this.shouldBeRunning) { + if (this.commandController) + await this.commandController.term().catch((err) => console.error(err)) + this.commandController = await this.startCommand() + await this.commandController.wait().catch((err) => console.error(err)) + await new Promise((resolve) => setTimeout(resolve, timeoutCounter)) + timeoutCounter += TIMEOUT_INCREMENT_MS + timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter) + } + }).catch((err) => { + console.error(asError(err)) + }) + } + async term(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + return this.stop(termOptions) + } + async stop(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + this.shouldBeRunning = false + await this.commandController + ?.term({ ...termOptions }) + .catch((e) => console.error(asError(e))) + this.commandController = null + } +} diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts new file mode 100644 index 000000000..8d0e6297a --- /dev/null +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -0,0 +1,208 @@ +import { HealthReceipt, Signals } from "../../../base/lib/types" + +import { HealthCheckResult } from "../health/checkFns" + +import { Trigger } from "../trigger" +import * as T from "../../../base/lib/types" +import { Mounts } from "./Mounts" +import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer" + +import { promisify } from "node:util" +import * as CP from "node:child_process" + +export { Daemon } from "./Daemon" +export { CommandController } from "./CommandController" +import { HealthDaemon } from "./HealthDaemon" +import { Daemon } from "./Daemon" +import { CommandController } from "./CommandController" + +export const cpExec = promisify(CP.exec) +export const cpExecFile = promisify(CP.execFile) +export type Ready = { + /** A human-readable display name for the health check. If null, the health check itself will be from the UI */ + display: string | null + /** + * @description The function to determine the health status of the daemon + * + * The SDK provides some built-in health checks. To see them, type sdk.healthCheck. + * + * @example + * ``` + fn: () => + sdk.healthCheck.checkPortListening(effects, 80, { + successMessage: 'service listening on port 80', + errorMessage: 'service is unreachable', + }) + * ``` + */ + fn: ( + spawnable: ExecSpawnable, + ) => Promise | HealthCheckResult + trigger?: Trigger +} + +type DaemonsParams< + Manifest extends T.SDKManifest, + Ids extends string, + Command extends string, + Id extends string, +> = { + /** The command line command to start the daemon */ + command: T.CommandType + /** Information about the subcontainer in which the daemon runs */ + subcontainer: + | { + /** The ID of the image. Must be one of the image IDs declared in the manifest */ + imageId: keyof Manifest["images"] & T.ImageId + /** + * Whether or not to share the `/run` directory with the parent container. + * This is useful if you are trying to connect to a service that exposes a unix domain socket or auth cookie via the `/run` directory + */ + sharedRun?: boolean + } + | SubContainer + /** For mounting the necessary volumes. Syntax: sdk.Mounts.of().addVolume() */ + mounts: Mounts + env?: Record + ready: Ready + /** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */ + requires: Exclude[] + sigtermTimeout?: number + onStdout?: (chunk: Buffer | string | any) => void + onStderr?: (chunk: Buffer | string | any) => void +} + +type ErrorDuplicateId = `The id '${Id}' is already used` + +export const runCommand = () => + CommandController.of() + +/** + * A class for defining and controlling the service daemons +```ts +Daemons.of({ + effects, + started, + interfaceReceipt, // Provide the interfaceReceipt to prove it was completed + healthReceipts, // Provide the healthReceipts or [] to prove they were at least considered +}).addDaemon('webui', { + command: 'hello-world', // The command to start the daemon + ready: { + display: 'Web Interface', + // The function to run to determine the health status of the daemon + fn: () => + checkPortListening(effects, 80, { + successMessage: 'The web interface is ready', + errorMessage: 'The web interface is not ready', + }), + }, + requires: [], +}) +``` + */ +export class Daemons + implements T.DaemonBuildable +{ + private constructor( + readonly effects: T.Effects, + readonly started: (onTerm: () => PromiseLike) => PromiseLike, + readonly daemons: Promise[], + readonly ids: Ids[], + readonly healthDaemons: HealthDaemon[], + ) {} + /** + * Returns an empty new Daemons class with the provided inputSpec. + * + * Call .addDaemon() on the returned class to add a daemon. + * + * Daemons run in the order they are defined, with latter daemons being capable of + * depending on prior daemons + * @param options + * @returns + */ + static of(options: { + effects: T.Effects + started: (onTerm: () => PromiseLike) => PromiseLike + healthReceipts: HealthReceipt[] + }) { + return new Daemons( + options.effects, + options.started, + [], + [], + [], + ) + } + /** + * Returns the complete list of daemons, including the one defined here + * @param id + * @param newDaemon + * @returns + */ + addDaemon( + // prettier-ignore + id: + "" extends Id ? never : + ErrorDuplicateId extends Id ? never : + Id extends Ids ? ErrorDuplicateId : + Id, + options: DaemonsParams, + ) { + const daemonIndex = this.daemons.length + const daemon = Daemon.of()( + this.effects, + options.subcontainer, + options.command, + { + ...options, + mounts: options.mounts.build(), + subcontainerName: id, + }, + ) + const healthDaemon = new HealthDaemon( + daemon, + daemonIndex, + options.requires + .map((x) => this.ids.indexOf(id as any)) + .filter((x) => x >= 0) + .map((id) => this.healthDaemons[id]), + id, + this.ids, + options.ready, + this.effects, + options.sigtermTimeout, + ) + const daemons = this.daemons.concat(daemon) + const ids = [...this.ids, id] as (Ids | Id)[] + const healthDaemons = [...this.healthDaemons, healthDaemon] + return new Daemons( + this.effects, + this.started, + daemons, + ids, + healthDaemons, + ) + } + + async build() { + const built = { + term: async () => { + try { + for (let result of await Promise.allSettled( + this.healthDaemons.map((x) => + x.term({ timeout: x.sigtermTimeout }), + ), + )) { + if (result.status === "rejected") { + console.error(result.reason) + } + } + } finally { + this.effects.setMainStatus({ status: "stopped" }) + } + }, + } + this.started(() => built.term()) + return built + } +} diff --git a/sdk/package/lib/mainFn/HealthDaemon.ts b/sdk/package/lib/mainFn/HealthDaemon.ts new file mode 100644 index 000000000..ac459f08c --- /dev/null +++ b/sdk/package/lib/mainFn/HealthDaemon.ts @@ -0,0 +1,164 @@ +import { HealthCheckResult } from "../health/checkFns" +import { defaultTrigger } from "../trigger/defaultTrigger" +import { Ready } from "./Daemons" +import { Daemon } from "./Daemon" +import { SetHealth, Effects } from "../../../base/lib/types" +import { DEFAULT_SIGTERM_TIMEOUT } from "." +import { asError } from "../../../base/lib/util/asError" + +const oncePromise = () => { + let resolve: (value: T) => void + const promise = new Promise((res) => { + resolve = res + }) + return { resolve: resolve!, promise } +} + +/** + * Wanted a structure that deals with controlling daemons by their health status + * States: + * -- Waiting for dependencies to be success + * -- Running: Daemon is running and the status is in the health + * + */ +export class HealthDaemon { + private _health: HealthCheckResult = { result: "starting", message: null } + private healthWatchers: Array<() => unknown> = [] + private running = false + private resolveReady: (() => void) | undefined + private readyPromise: Promise + constructor( + private readonly daemon: Promise, + readonly daemonIndex: number, + private readonly dependencies: HealthDaemon[], + readonly id: string, + readonly ids: string[], + readonly ready: Ready, + readonly effects: Effects, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, + ) { + this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve)) + this.updateStatus() + this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) + } + + /** Run after we want to do cleanup */ + async term(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + this.healthWatchers = [] + this.running = false + this.healthCheckCleanup?.() + + await this.daemon.then((d) => + d.term({ + timeout: this.sigtermTimeout, + ...termOptions, + }), + ) + } + + /** Want to add another notifier that the health might have changed */ + addWatcher(watcher: () => unknown) { + this.healthWatchers.push(watcher) + } + + get health() { + return Object.freeze(this._health) + } + + private async changeRunning(newStatus: boolean) { + if (this.running === newStatus) return + + this.running = newStatus + + if (newStatus) { + ;(await this.daemon).start() + this.setupHealthCheck() + } else { + ;(await this.daemon).stop() + this.turnOffHealthCheck() + + this.setHealth({ result: "starting", message: null }) + } + } + + private healthCheckCleanup: (() => null) | null = null + private turnOffHealthCheck() { + this.healthCheckCleanup?.() + } + private async setupHealthCheck() { + if (this.healthCheckCleanup) return + const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ + lastResult: this._health.result, + })) + + const { promise: status, resolve: setStatus } = oncePromise<{ + done: true + }>() + new Promise(async () => { + for ( + let res = await Promise.race([status, trigger.next()]); + !res.done; + res = await Promise.race([status, trigger.next()]) + ) { + const handle = (await this.daemon).subContainerHandle + + if (handle) { + const response: HealthCheckResult = await Promise.resolve( + this.ready.fn(handle), + ).catch((err) => { + console.error(asError(err)) + return { + result: "failure", + message: "message" in err ? err.message : String(err), + } + }) + if ( + this.resolveReady && + (response.result === "success" || response.result === "disabled") + ) { + this.resolveReady() + } + await this.setHealth(response) + } else { + await this.setHealth({ + result: "failure", + message: "Daemon not running", + }) + } + } + }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) + + this.healthCheckCleanup = () => { + setStatus({ done: true }) + this.healthCheckCleanup = null + return null + } + } + + onReady() { + return this.readyPromise + } + + private async setHealth(health: HealthCheckResult) { + this._health = health + this.healthWatchers.forEach((watcher) => watcher()) + const display = this.ready.display + const result = health.result + if (!display) { + return + } + await this.effects.setHealth({ + ...health, + id: this.id, + name: display, + } as SetHealth) + } + + private async updateStatus() { + const healths = this.dependencies.map((d) => d._health) + this.changeRunning(healths.every((x) => x.result === "success")) + } +} diff --git a/sdk/package/lib/mainFn/Mounts.ts b/sdk/package/lib/mainFn/Mounts.ts new file mode 100644 index 000000000..799140871 --- /dev/null +++ b/sdk/package/lib/mainFn/Mounts.ts @@ -0,0 +1,137 @@ +import * as T from "../../../base/lib/types" +import { MountOptions } from "../util/SubContainer" + +type MountArray = { path: string; options: MountOptions }[] + +export class Mounts { + private constructor( + readonly volumes: { + id: Manifest["volumes"][number] + subpath: string | null + mountpoint: string + readonly: boolean + }[], + readonly assets: { + id: Manifest["assets"][number] + subpath: string | null + mountpoint: string + }[], + readonly dependencies: { + dependencyId: string + volumeId: string + subpath: string | null + mountpoint: string + readonly: boolean + }[], + ) {} + + static of() { + return new Mounts([], [], []) + } + + addVolume( + /** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */ + id: Manifest["volumes"][number], + /** The path within the volume to mount. Use `null` to mount the entire volume */ + subpath: string | null, + /** Where to mount the volume. e.g. /data */ + mountpoint: string, + /** Whether or not the volume should be readonly for this daemon */ + readonly: boolean, + ) { + this.volumes.push({ + id, + subpath, + mountpoint, + readonly, + }) + return this + } + + addAssets( + /** The ID of the asset directory to mount. This is typically the same as the folder name in your assets directory */ + id: Manifest["assets"][number], + /** The path within the asset directory to mount. Use `null` to mount the entire volume */ + subpath: string | null, + /** Where to mount the asset. e.g. /asset */ + mountpoint: string, + ) { + this.assets.push({ + id, + subpath, + mountpoint, + }) + return this + } + + addDependency( + /** The ID of the dependency service */ + dependencyId: keyof Manifest["dependencies"] & string, + /** The ID of the volume belonging to the dependency service to mount */ + volumeId: DependencyManifest["volumes"][number], + /** The path within the dependency's volume to mount. Use `null` to mount the entire volume */ + subpath: string | null, + /** Where to mount the dependency's volume. e.g. /service-id */ + mountpoint: string, + /** Whether or not the volume should be readonly for this daemon */ + readonly: boolean, + ) { + this.dependencies.push({ + dependencyId, + volumeId, + subpath, + mountpoint, + readonly, + }) + return this + } + + build(): MountArray { + const mountpoints = new Set() + for (let mountpoint of this.volumes + .map((v) => v.mountpoint) + .concat(this.assets.map((a) => a.mountpoint)) + .concat(this.dependencies.map((d) => d.mountpoint))) { + if (mountpoints.has(mountpoint)) { + throw new Error( + `cannot mount more than once to mountpoint ${mountpoint}`, + ) + } + mountpoints.add(mountpoint) + } + return ([] as MountArray) + .concat( + this.volumes.map((v) => ({ + path: v.mountpoint, + options: { + type: "volume", + id: v.id, + subpath: v.subpath, + readonly: v.readonly, + }, + })), + ) + .concat( + this.assets.map((a) => ({ + path: a.mountpoint, + options: { + type: "assets", + id: a.id, + subpath: a.subpath, + }, + })), + ) + .concat( + this.dependencies.map((d) => ({ + path: d.mountpoint, + options: { + type: "pointer", + packageId: d.dependencyId, + volumeId: d.volumeId, + subpath: d.subpath, + readonly: d.readonly, + }, + })), + ) + } +} diff --git a/sdk/package/lib/mainFn/index.ts b/sdk/package/lib/mainFn/index.ts new file mode 100644 index 000000000..be30c652d --- /dev/null +++ b/sdk/package/lib/mainFn/index.ts @@ -0,0 +1,27 @@ +import * as T from "../../../base/lib/types" +import { Daemons } from "./Daemons" +import "../../../base/lib/interfaces/ServiceInterfaceBuilder" +import "../../../base/lib/interfaces/Origin" + +export const DEFAULT_SIGTERM_TIMEOUT = 30_000 +/** + * Used to ensure that the main function is running with the valid proofs. + * We first do the folowing order of things + * 1. We get the interfaces + * 2. We setup all the commands to setup the system + * 3. We create the health checks + * 4. We setup the daemons init system + * @param fn + * @returns + */ +export const setupMain = ( + fn: (o: { + effects: T.Effects + started(onTerm: () => PromiseLike): PromiseLike + }) => Promise>, +): T.ExpectedExports.main => { + return async (options) => { + const result = await fn(options) + return result + } +} diff --git a/sdk/package/lib/manifest/setupManifest.ts b/sdk/package/lib/manifest/setupManifest.ts new file mode 100644 index 000000000..3cd4f8bfb --- /dev/null +++ b/sdk/package/lib/manifest/setupManifest.ts @@ -0,0 +1,96 @@ +import * as T from "../../../base/lib/types" +import { ImageConfig, ImageId, VolumeId } from "../../../base/lib/types" +import { + SDKManifest, + SDKImageInputSpec, +} from "../../../base/lib/types/ManifestTypes" +import { SDKVersion } from "../StartSdk" +import { VersionGraph } from "../version/VersionGraph" +import { execSync } from "child_process" + +/** + * @description Use this function to define critical information about your package + * + * @param manifest Static properties of the package + */ +export function setupManifest< + Id extends string, + VolumesTypes extends VolumeId, + AssetTypes extends VolumeId, + Manifest extends { + id: Id + assets: AssetTypes[] + volumes: VolumesTypes[] + } & SDKManifest, +>(manifest: Manifest & SDKManifest): Manifest { + return manifest +} + +export function buildManifest< + Id extends string, + Version extends string, + Dependencies extends Record, + VolumesTypes extends VolumeId, + AssetTypes extends VolumeId, + ImagesTypes extends ImageId, + Manifest extends { + dependencies: Dependencies + id: Id + assets: AssetTypes[] + images: Record + volumes: VolumesTypes[] + }, +>( + versions: VersionGraph, + manifest: SDKManifest & Manifest, +): Manifest & T.Manifest { + const images = Object.entries(manifest.images).reduce( + (images, [k, v]) => { + v.arch = v.arch || ["aarch64", "x86_64"] + if (v.emulateMissingAs === undefined) + v.emulateMissingAs = (v.arch as string[]).includes("aarch64") + ? "aarch64" + : v.arch[0] || null + images[k] = v as ImageConfig + return images + }, + {} as { [k: string]: ImageConfig }, + ) + return { + ...manifest, + osVersion: SDKVersion, + version: versions.current.options.version, + releaseNotes: versions.current.options.releaseNotes, + satisfies: versions.current.options.satisfies || [], + canMigrateTo: versions.canMigrateTo().toString(), + canMigrateFrom: versions.canMigrateFrom().toString(), + images, + alerts: { + install: manifest.alerts?.install || null, + update: manifest.alerts?.update || null, + uninstall: manifest.alerts?.uninstall || null, + restore: manifest.alerts?.restore || null, + start: manifest.alerts?.start || null, + stop: manifest.alerts?.stop || null, + }, + hardwareRequirements: { + device: manifest.hardwareRequirements?.device || [], + ram: manifest.hardwareRequirements?.ram || null, + arch: + manifest.hardwareRequirements?.arch === undefined + ? Object.values(images).reduce( + (arch, inputSpec) => { + if (inputSpec.emulateMissingAs) { + return arch + } + if (arch === null) { + return inputSpec.arch + } + return arch.filter((a) => inputSpec.arch.includes(a)) + }, + null as string[] | null, + ) + : manifest.hardwareRequirements?.arch, + }, + } +} diff --git a/sdk/package/lib/store/getStore.ts b/sdk/package/lib/store/getStore.ts new file mode 100644 index 000000000..3bde46a25 --- /dev/null +++ b/sdk/package/lib/store/getStore.ts @@ -0,0 +1,61 @@ +import { Effects } from "../../../base/lib/Effects" +import { PathBuilder, extractJsonPath } from "../util" + +export class GetStore { + constructor( + readonly effects: Effects, + readonly path: PathBuilder, + readonly options: { + /** Defaults to what ever the package currently in */ + packageId?: string | undefined + } = {}, + ) {} + + /** + * Returns the value of Store at the provided path. Restart the service if the value changes + */ + const() { + return this.effects.store.get({ + ...this.options, + path: extractJsonPath(this.path), + callback: () => this.effects.constRetry(), + }) + } + /** + * Returns the value of Store at the provided path. Does nothing if the value changes + */ + once() { + return this.effects.store.get({ + ...this.options, + path: extractJsonPath(this.path), + }) + } + + /** + * Watches the value of Store at the provided path. Takes a custom callback function to run whenever the value changes + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.store.get({ + ...this.options, + path: extractJsonPath(this.path), + callback: () => callback(), + }) + await waitForNext + } + } +} +export function getStore( + effects: Effects, + path: PathBuilder, + options: { + /** Defaults to what ever the package currently in */ + packageId?: string | undefined + } = {}, +) { + return new GetStore(effects, path, options) +} diff --git a/sdk/package/lib/store/setupExposeStore.ts b/sdk/package/lib/store/setupExposeStore.ts new file mode 100644 index 000000000..7f5415bd7 --- /dev/null +++ b/sdk/package/lib/store/setupExposeStore.ts @@ -0,0 +1,27 @@ +import { ExposedStorePaths } from "../../../base/lib/types" +import { + PathBuilder, + extractJsonPath, + pathBuilder, +} from "../../../base/lib/util/PathBuilder" + +/** + * @description Use this function to determine which Store values to expose and make available to other services running on StartOS. Store values not exposed here will be kept private. Use the type safe pathBuilder to traverse your Store's structure. + * @example + * In this example, we expose the hypothetical Store values "adminPassword" and "nameLastUpdatedAt". + * + * ``` + export const exposedStore = setupExposeStore((pathBuilder) => [ + pathBuilder.adminPassword + pathBuilder.nameLastUpdatedAt, + ]) + * ``` + */ +export const setupExposeStore = >( + fn: (pathBuilder: PathBuilder) => PathBuilder[], +) => { + return fn(pathBuilder()).map( + (x) => extractJsonPath(x) as string, + ) as ExposedStorePaths +} +export { ExposedStorePaths } diff --git a/sdk/package/lib/test/health.readyCheck.test.ts b/sdk/package/lib/test/health.readyCheck.test.ts new file mode 100644 index 000000000..49efcc759 --- /dev/null +++ b/sdk/package/lib/test/health.readyCheck.test.ts @@ -0,0 +1,17 @@ +import { containsAddress } from "../health/checkFns/checkPortListening" + +describe("Health ready check", () => { + it("Should be able to parse an example information", () => { + let input = ` + + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21634478 1 0000000000000000 100 0 0 10 0 + 1: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21634477 1 0000000000000000 100 0 0 10 0 + 2: 0B00007F:9671 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21635458 1 0000000000000000 100 0 0 10 0 + 3: 00000000:0D73 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21634479 1 0000000000000000 100 0 0 10 0 + ` + + expect(containsAddress(input, 80)).toBe(true) + expect(containsAddress(input, 1234)).toBe(false) + }) +}) diff --git a/sdk/package/lib/test/host.test.ts b/sdk/package/lib/test/host.test.ts new file mode 100644 index 000000000..88ca7c4b6 --- /dev/null +++ b/sdk/package/lib/test/host.test.ts @@ -0,0 +1,29 @@ +import { ServiceInterfaceBuilder } from "../../../base/lib/interfaces/ServiceInterfaceBuilder" +import { Effects } from "../../../base/lib/Effects" +import { sdk } from "../test/output.sdk" + +describe("host", () => { + test("Testing that the types work", () => { + async function test(effects: Effects) { + const foo = sdk.MultiHost.of(effects, "foo") + const fooOrigin = await foo.bindPort(80, { + protocol: "http" as const, + preferredExternalPort: 80, + }) + const fooInterface = new ServiceInterfaceBuilder({ + effects, + name: "Foo", + id: "foo", + description: "A Foo", + type: "ui", + username: "bar", + path: "/baz", + search: { qux: "yes" }, + schemeOverride: null, + masked: false, + }) + + await fooOrigin.export([fooInterface]) + } + }) +}) diff --git a/sdk/package/lib/test/inputSpecBuilder.test.ts b/sdk/package/lib/test/inputSpecBuilder.test.ts new file mode 100644 index 000000000..4179a9f04 --- /dev/null +++ b/sdk/package/lib/test/inputSpecBuilder.test.ts @@ -0,0 +1,890 @@ +import { testOutput } from "./output.test" +import { InputSpec } from "../../../base/lib/actions/input/builder/inputSpec" +import { List } from "../../../base/lib/actions/input/builder/list" +import { Value } from "../../../base/lib/actions/input/builder/value" +import { Variants } from "../../../base/lib/actions/input/builder/variants" +import { ValueSpec } from "../../../base/lib/actions/input/inputSpecTypes" +import { setupManifest } from "../manifest/setupManifest" +import { StartSdk } from "../StartSdk" + +describe("builder tests", () => { + test("text", async () => { + const bitcoinPropertiesBuilt: { + "peer-tor-address": ValueSpec + } = await InputSpec.of({ + "peer-tor-address": Value.text({ + name: "Peer tor address", + description: "The Tor address of the peer interface", + required: true, + default: null, + }), + }).build({} as any) + expect(bitcoinPropertiesBuilt).toMatchObject({ + "peer-tor-address": { + type: "text", + description: "The Tor address of the peer interface", + warning: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: null, + patterns: [], + disabled: false, + inputmode: "text", + name: "Peer tor address", + required: true, + default: null, + }, + }) + }) +}) + +describe("values", () => { + test("toggle", async () => { + const value = Value.toggle({ + name: "Testing", + description: null, + warning: null, + default: false, + }) + const validator = value.validator + validator.unsafeCast(false) + testOutput()(null) + }) + test("text", async () => { + const value = Value.text({ + name: "Testing", + required: true, + default: null, + }) + const validator = value.validator + const rawIs = await value.build({} as any) + validator.unsafeCast("test text") + expect(() => validator.unsafeCast(null)).toThrowError() + testOutput()(null) + }) + test("text with default", async () => { + const value = Value.text({ + name: "Testing", + required: true, + default: "this is a default value", + }) + const validator = value.validator + const rawIs = await value.build({} as any) + validator.unsafeCast("test text") + expect(() => validator.unsafeCast(null)).toThrowError() + testOutput()(null) + }) + test("optional text", async () => { + const value = Value.text({ + name: "Testing", + required: false, + default: null, + }) + const validator = value.validator + const rawIs = await value.build({} as any) + validator.unsafeCast("test text") + validator.unsafeCast(null) + testOutput()(null) + }) + test("color", async () => { + const value = Value.color({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + }) + const validator = value.validator + validator.unsafeCast("#000000") + testOutput()(null) + }) + test("datetime", async () => { + const value = Value.datetime({ + name: "Testing", + required: true, + default: null, + description: null, + warning: null, + inputmode: "date", + min: null, + max: null, + }) + const validator = value.validator + validator.unsafeCast("2021-01-01") + testOutput()(null) + }) + test("optional datetime", async () => { + const value = Value.datetime({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + inputmode: "date", + min: null, + max: null, + }) + const validator = value.validator + validator.unsafeCast("2021-01-01") + testOutput()(null) + }) + test("textarea", async () => { + const value = Value.textarea({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + minLength: null, + maxLength: null, + placeholder: null, + }) + const validator = value.validator + validator.unsafeCast("test text") + testOutput()(null) + }) + test("number", async () => { + const value = Value.number({ + name: "Testing", + required: true, + default: null, + integer: false, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + }) + const validator = value.validator + validator.unsafeCast(2) + testOutput()(null) + }) + test("optional number", async () => { + const value = Value.number({ + name: "Testing", + required: false, + default: null, + integer: false, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + }) + const validator = value.validator + validator.unsafeCast(2) + testOutput()(null) + }) + test("select", async () => { + const value = Value.select({ + name: "Testing", + default: "a", + values: { + a: "A", + b: "B", + }, + description: null, + warning: null, + }) + const validator = value.validator + validator.unsafeCast("a") + validator.unsafeCast("b") + expect(() => validator.unsafeCast("c")).toThrowError() + testOutput()(null) + }) + test("nullable select", async () => { + const value = Value.select({ + name: "Testing", + default: "a", + values: { + a: "A", + b: "B", + }, + description: null, + warning: null, + }) + const validator = value.validator + validator.unsafeCast("a") + validator.unsafeCast("b") + testOutput()(null) + }) + test("multiselect", async () => { + const value = Value.multiselect({ + name: "Testing", + values: { + a: "A", + b: "B", + }, + default: [], + description: null, + warning: null, + minLength: null, + maxLength: null, + }) + const validator = value.validator + validator.unsafeCast([]) + validator.unsafeCast(["a", "b"]) + + expect(() => validator.unsafeCast(["e"])).toThrowError() + expect(() => validator.unsafeCast([4])).toThrowError() + testOutput>()(null) + }) + test("object", async () => { + const value = Value.object( + { + name: "Testing", + description: null, + }, + InputSpec.of({ + a: Value.toggle({ + name: "test", + description: null, + warning: null, + default: false, + }), + }), + ) + const validator = value.validator + validator.unsafeCast({ a: true }) + testOutput()(null) + }) + test("union", async () => { + const value = Value.union( + { + name: "Testing", + default: "a", + description: null, + warning: null, + }, + Variants.of({ + a: { + name: "a", + spec: InputSpec.of({ + b: Value.toggle({ + name: "b", + description: null, + warning: null, + default: false, + }), + }), + }, + }), + ) + const validator = value.validator + validator.unsafeCast({ selection: "a", value: { b: false } }) + type Test = typeof validator._TYPE + testOutput< + Test, + { + selection: "a" + value: { + b: boolean + } + other?: {} + } + >()(null) + }) + + describe("dynamic", () => { + const fakeOptions = { + inputSpec: "inputSpec", + effects: "effects", + utils: "utils", + } as any + test("toggle", async () => { + const value = Value.dynamicToggle(async () => ({ + name: "Testing", + description: null, + warning: null, + default: false, + })) + const validator = value.validator + validator.unsafeCast(false) + expect(() => validator.unsafeCast(null)).toThrowError() + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + description: null, + warning: null, + default: false, + }) + }) + test("text", async () => { + const value = Value.dynamicText(async () => ({ + name: "Testing", + required: false, + default: null, + })) + const validator = value.validator + const rawIs = await value.build({} as any) + validator.unsafeCast("test text") + validator.unsafeCast(null) + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: false, + default: null, + }) + }) + test("text with default", async () => { + const value = Value.dynamicText(async () => ({ + name: "Testing", + required: false, + default: "this is a default value", + })) + const validator = value.validator + validator.unsafeCast("test text") + validator.unsafeCast(null) + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: false, + default: "this is a default value", + }) + }) + test("optional text", async () => { + const value = Value.dynamicText(async () => ({ + name: "Testing", + required: false, + default: null, + })) + const validator = value.validator + const rawIs = await value.build({} as any) + validator.unsafeCast("test text") + validator.unsafeCast(null) + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: false, + default: null, + }) + }) + test("color", async () => { + const value = Value.dynamicColor(async () => ({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + })) + const validator = value.validator + validator.unsafeCast("#000000") + validator.unsafeCast(null) + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + }) + }) + test("datetime", async () => { + const sdk = StartSdk.of() + .withManifest( + setupManifest({ + id: "testOutput", + title: "", + license: "", + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: {}, + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + "remote-test": { + description: "", + optional: true, + s9pk: "https://example.com/remote-test.s9pk", + }, + }, + }), + ) + .withStore<{ test: "a" }>() + .build(true) + + const value = Value.dynamicDatetime<{ test: "a" }>( + async ({ effects }) => { + ;async () => { + ;(await sdk.store + .getOwn(effects, sdk.StorePath.test) + .once()) satisfies "a" + } + + return { + name: "Testing", + required: true, + default: null, + inputmode: "date", + } + }, + ) + const validator = value.validator + validator.unsafeCast("2021-01-01") + validator.unsafeCast(null) + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: true, + default: null, + description: null, + warning: null, + inputmode: "date", + }) + }) + test("textarea", async () => { + const value = Value.dynamicTextarea(async () => ({ + name: "Testing", + required: false, + default: null, + description: null, + warning: null, + minLength: null, + maxLength: null, + placeholder: null, + })) + const validator = value.validator + validator.unsafeCast("test text") + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: false, + }) + }) + test("number", async () => { + const value = Value.dynamicNumber(() => ({ + name: "Testing", + required: true, + default: null, + integer: false, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + })) + const validator = value.validator + validator.unsafeCast(2) + validator.unsafeCast(null) + expect(() => validator.unsafeCast("null")).toThrowError() + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + required: true, + }) + }) + test("select", async () => { + const value = Value.dynamicSelect(() => ({ + name: "Testing", + default: "a", + values: { + a: "A", + b: "B", + }, + description: null, + warning: null, + })) + const validator = value.validator + validator.unsafeCast("a") + validator.unsafeCast("b") + validator.unsafeCast("c") + testOutput()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + }) + }) + test("multiselect", async () => { + const value = Value.dynamicMultiselect(() => ({ + name: "Testing", + values: { + a: "A", + b: "B", + }, + default: [], + description: null, + warning: null, + minLength: null, + maxLength: null, + })) + const validator = value.validator + validator.unsafeCast([]) + validator.unsafeCast(["a", "b"]) + validator.unsafeCast(["c"]) + + expect(() => validator.unsafeCast([4])).toThrowError() + expect(() => validator.unsafeCast(null)).toThrowError() + testOutput>()(null) + expect(await value.build(fakeOptions)).toMatchObject({ + name: "Testing", + default: [], + }) + }) + }) + describe("filtering", () => { + test("union", async () => { + const value = Value.filteredUnion( + () => ["a", "c"], + { + name: "Testing", + default: "a", + description: null, + warning: null, + }, + Variants.of({ + a: { + name: "a", + spec: InputSpec.of({ + b: Value.toggle({ + name: "b", + description: null, + warning: null, + default: false, + }), + }), + }, + b: { + name: "b", + spec: InputSpec.of({ + b: Value.toggle({ + name: "b", + description: null, + warning: null, + default: false, + }), + }), + }, + }), + ) + const validator = value.validator + validator.unsafeCast({ selection: "a", value: { b: false } }) + type Test = typeof validator._TYPE + testOutput< + Test, + | { + selection: "a" + value: { + b: boolean + } + other?: { + b?: { + b?: boolean + } + } + } + | { + selection: "b" + value: { + b: boolean + } + other?: { + a?: { + b?: boolean + } + } + } + >()(null) + + const built = await value.build({} as any) + expect(built).toMatchObject({ + name: "Testing", + variants: { + b: {}, + }, + }) + expect(built).toMatchObject({ + name: "Testing", + variants: { + a: {}, + b: {}, + }, + }) + expect(built).toMatchObject({ + name: "Testing", + variants: { + a: {}, + b: {}, + }, + disabled: ["a", "c"], + }) + }) + }) + test("dynamic union", async () => { + const value = Value.dynamicUnion( + () => ({ + disabled: ["a", "c"], + name: "Testing", + default: "b", + description: null, + warning: null, + }), + Variants.of({ + a: { + name: "a", + spec: InputSpec.of({ + b: Value.toggle({ + name: "b", + description: null, + warning: null, + default: false, + }), + }), + }, + b: { + name: "b", + spec: InputSpec.of({ + b: Value.toggle({ + name: "b", + description: null, + warning: null, + default: false, + }), + }), + }, + }), + ) + const validator = value.validator + validator.unsafeCast({ selection: "a", value: { b: false } }) + type Test = typeof validator._TYPE + testOutput< + Test, + | { + selection: "a" + value: { + b: boolean + } + other?: { + b?: { + b?: boolean + } + } + } + | { + selection: "b" + value: { + b: boolean + } + other?: { + a?: { + b?: boolean + } + } + } + >()(null) + + const built = await value.build({} as any) + expect(built).toMatchObject({ + name: "Testing", + variants: { + b: {}, + }, + }) + expect(built).toMatchObject({ + name: "Testing", + variants: { + a: {}, + b: {}, + }, + }) + expect(built).toMatchObject({ + name: "Testing", + variants: { + a: {}, + b: {}, + }, + disabled: ["a", "c"], + }) + }) +}) + +describe("Builder List", () => { + test("obj", async () => { + const value = Value.list( + List.obj( + { + name: "test", + }, + { + spec: InputSpec.of({ + test: Value.toggle({ + name: "test", + description: null, + warning: null, + default: false, + }), + }), + }, + ), + ) + const validator = value.validator + validator.unsafeCast([{ test: true }]) + testOutput()(null) + }) + test("text", async () => { + const value = Value.list( + List.text( + { + name: "test", + }, + { + patterns: [], + }, + ), + ) + const validator = value.validator + validator.unsafeCast(["test", "text"]) + testOutput()(null) + }) + describe("dynamic", () => { + test("text", async () => { + const value = Value.list( + List.dynamicText(() => ({ + name: "test", + spec: { patterns: [] }, + })), + ) + const validator = value.validator + validator.unsafeCast(["test", "text"]) + expect(() => validator.unsafeCast([3, 4])).toThrowError() + expect(() => validator.unsafeCast(null)).toThrowError() + testOutput()(null) + expect(await value.build({} as any)).toMatchObject({ + name: "test", + spec: { patterns: [] }, + }) + }) + }) +}) + +describe("Nested nullable values", () => { + test("Testing text", async () => { + const value = InputSpec.of({ + a: Value.text({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + required: false, + default: null, + }), + }) + const validator = value.validator + validator.unsafeCast({ a: null }) + validator.unsafeCast({ a: "test" }) + expect(() => validator.unsafeCast({ a: 4 })).toThrowError() + testOutput()(null) + }) + test("Testing number", async () => { + const value = InputSpec.of({ + a: Value.number({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + required: false, + default: null, + warning: null, + placeholder: null, + integer: false, + min: null, + max: null, + step: null, + units: null, + }), + }) + const validator = value.validator + validator.unsafeCast({ a: null }) + validator.unsafeCast({ a: 5 }) + expect(() => validator.unsafeCast({ a: "4" })).toThrowError() + testOutput()(null) + }) + test("Testing color", async () => { + const value = InputSpec.of({ + a: Value.color({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + required: false, + default: null, + warning: null, + }), + }) + const validator = value.validator + validator.unsafeCast({ a: null }) + validator.unsafeCast({ a: "5" }) + expect(() => validator.unsafeCast({ a: 4 })).toThrowError() + testOutput()(null) + }) + test("Testing select", async () => { + const value = InputSpec.of({ + a: Value.select({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + default: "a", + warning: null, + values: { + a: "A", + }, + }), + }) + const higher = await Value.select({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + default: "a", + warning: null, + values: { + a: "A", + }, + }).build({} as any) + + const validator = value.validator + validator.unsafeCast({ a: "a" }) + expect(() => validator.unsafeCast({ a: "4" })).toThrowError() + testOutput()(null) + }) + test("Testing multiselect", async () => { + const value = InputSpec.of({ + a: Value.multiselect({ + name: "Temp Name", + description: + "If no name is provided, the name from inputSpec will be used", + + warning: null, + default: [], + values: { + a: "A", + }, + minLength: null, + maxLength: null, + }), + }) + const validator = value.validator + validator.unsafeCast({ a: [] }) + validator.unsafeCast({ a: ["a"] }) + expect(() => validator.unsafeCast({ a: ["4"] })).toThrowError() + expect(() => validator.unsafeCast({ a: "4" })).toThrowError() + testOutput()(null) + }) +}) diff --git a/sdk/package/lib/test/makeOutput.ts b/sdk/package/lib/test/makeOutput.ts new file mode 100644 index 000000000..434484be9 --- /dev/null +++ b/sdk/package/lib/test/makeOutput.ts @@ -0,0 +1,428 @@ +import { oldSpecToBuilder } from "../../scripts/oldSpecToBuilder" + +oldSpecToBuilder( + // Make the location + "./lib/test/output.ts", + // Put the inputSpec here + { + mediasources: { + type: "list", + subtype: "enum", + name: "Media Sources", + description: "List of Media Sources to use with Jellyfin", + range: "[1,*)", + default: ["nextcloud"], + spec: { + values: ["nextcloud", "filebrowser"], + "value-names": { + nextcloud: "NextCloud", + filebrowser: "File Browser", + }, + }, + }, + testListUnion: { + type: "list", + subtype: "union", + name: "Lightning Nodes", + description: "List of Lightning Network node instances to manage", + range: "[1,*)", + default: ["lnd"], + spec: { + type: "string", + "display-as": "{{name}}", + "unique-by": "name", + name: "Node Implementation", + tag: { + id: "type", + name: "Type", + description: + "- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n", + "variant-names": { + lnd: "Lightning Network Daemon (LND)", + "c-lightning": "Core Lightning (CLN)", + }, + }, + default: "lnd", + variants: { + lnd: { + name: { + type: "string", + name: "Node Name", + description: "Name of this node in the list", + default: "LND Wrapper", + nullable: false, + }, + }, + }, + }, + }, + rpc: { + type: "object", + name: "RPC Settings", + description: "RPC configuration options.", + spec: { + enable: { + type: "boolean", + name: "Enable", + description: "Allow remote RPC requests.", + default: true, + }, + username: { + type: "string", + nullable: false, + name: "Username", + description: "The username for connecting to Bitcoin over RPC.", + default: "bitcoin", + masked: true, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": + "Must be alphanumeric (can contain underscore).", + }, + password: { + type: "string", + nullable: false, + name: "RPC Password", + description: "The password for connecting to Bitcoin over RPC.", + default: { + charset: "a-z,2-7", + len: 20, + }, + pattern: '^[^\\n"]*$', + "pattern-description": + "Must not contain newline or quote characters.", + copyable: true, + masked: true, + }, + bio: { + type: "string", + nullable: false, + name: "Username", + description: "The username for connecting to Bitcoin over RPC.", + default: "bitcoin", + masked: true, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": + "Must be alphanumeric (can contain underscore).", + textarea: true, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced RPC Settings", + spec: { + auth: { + name: "Authorization", + description: + "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + type: "list", + subtype: "string", + default: [], + spec: { + pattern: + "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + "pattern-description": + 'Each item must be of the form ":$".', + masked: false, + }, + range: "[0,*)", + }, + serialversion: { + name: "Serialization Version", + description: + "Return raw transaction or block hex with Segwit or non-SegWit serialization.", + type: "enum", + values: ["non-segwit", "segwit"], + "value-names": {}, + default: "segwit", + }, + servertimeout: { + name: "Rpc Server Timeout", + description: + "Number of seconds after which an uncompleted RPC call will time out.", + type: "number", + nullable: false, + range: "[5,300]", + integral: true, + units: "seconds", + default: 30, + }, + threads: { + name: "Threads", + description: + "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + type: "number", + nullable: false, + default: 16, + range: "[1,64]", + integral: true, + }, + workqueue: { + name: "Work Queue", + description: + "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + type: "number", + nullable: false, + default: 128, + range: "[8,256]", + integral: true, + units: "requests", + }, + }, + }, + }, + }, + "zmq-enabled": { + type: "boolean", + name: "ZeroMQ Enabled", + description: "Enable the ZeroMQ interface", + default: true, + }, + txindex: { + type: "boolean", + name: "Transaction Index", + description: "Enable the Transaction Index (txindex)", + default: true, + }, + wallet: { + type: "object", + name: "Wallet", + description: "Wallet Settings", + spec: { + enable: { + name: "Enable Wallet", + description: "Load the wallet and enable wallet RPC calls.", + type: "boolean", + default: true, + }, + avoidpartialspends: { + name: "Avoid Partial Spends", + description: + "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + type: "boolean", + default: true, + }, + discardfee: { + name: "Discard Change Tolerance", + description: + "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + type: "number", + nullable: false, + default: 0.0001, + range: "[0,.01]", + integral: false, + units: "BTC/kB", + }, + }, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced Settings", + spec: { + mempool: { + type: "object", + name: "Mempool", + description: "Mempool Settings", + spec: { + mempoolfullrbf: { + name: "Enable Full RBF", + description: + "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.md#notice-of-new-option-for-transaction-replacement-policies", + type: "boolean", + default: false, + }, + persistmempool: { + type: "boolean", + name: "Persist Mempool", + description: "Save the mempool on shutdown and load on restart.", + default: true, + }, + maxmempool: { + type: "number", + nullable: false, + name: "Max Mempool Size", + description: + "Keep the transaction memory pool below megabytes.", + range: "[1,*)", + integral: true, + units: "MiB", + default: 300, + }, + mempoolexpiry: { + type: "number", + nullable: false, + name: "Mempool Expiration", + description: + "Do not keep transactions in the mempool longer than hours.", + range: "[1,*)", + integral: true, + units: "Hr", + default: 336, + }, + }, + }, + peers: { + type: "object", + name: "Peers", + description: "Peer Connection Settings", + spec: { + listen: { + type: "boolean", + name: "Make Public", + description: + "Allow other nodes to find your server on the network.", + default: true, + }, + onlyconnect: { + type: "boolean", + name: "Disable Peer Discovery", + description: "Only connect to specified peers.", + default: false, + }, + onlyonion: { + type: "boolean", + name: "Disable Clearnet", + description: "Only connect to peers over Tor.", + default: false, + }, + addnode: { + name: "Add Nodes", + description: "Add addresses of nodes to connect to.", + type: "list", + subtype: "object", + range: "[0,*)", + default: [], + spec: { + "unique-by": null, + spec: { + hostname: { + type: "string", + nullable: true, + name: "Hostname", + description: "Domain or IP address of bitcoin peer", + pattern: + "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + "pattern-description": + "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + masked: false, + }, + port: { + type: "number", + nullable: true, + name: "Port", + description: + "Port that peer is listening on for inbound p2p connections", + range: "[0,65535]", + integral: true, + }, + }, + }, + }, + }, + }, + dbcache: { + type: "number", + nullable: true, + name: "Database Cache", + description: + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + warning: + "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + range: "(0,*)", + integral: true, + units: "MiB", + }, + pruning: { + type: "union", + name: "Pruning Settings", + description: + "Blockchain Pruning Options\nReduce the blockchain size on disk\n", + warning: + "If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n", + tag: { + id: "mode", + name: "Pruning Mode", + description: + '- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n', + "variant-names": { + disabled: "Disabled", + automatic: "Automatic", + manual: "Manual", + }, + }, + variants: { + disabled: {}, + automatic: { + size: { + type: "number", + nullable: false, + name: "Max Chain Size", + description: "Limit of blockchain size on disk.", + warning: + "Increasing this value will require re-syncing your node.", + default: 550, + range: "[550,1000000)", + integral: true, + units: "MiB", + }, + }, + manual: { + size: { + type: "number", + nullable: false, + name: "Failsafe Chain Size", + description: "Prune blockchain if size expands beyond this.", + default: 65536, + range: "[550,1000000)", + integral: true, + units: "MiB", + }, + }, + }, + default: "disabled", + }, + blockfilters: { + type: "object", + name: "Block Filters", + description: "Settings for storing and serving compact block filters", + spec: { + blockfilterindex: { + type: "boolean", + name: "Compute Compact Block Filters (BIP158)", + description: + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + default: true, + }, + peerblockfilters: { + type: "boolean", + name: "Serve Compact Block Filters to Peers (BIP157)", + description: + "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + default: false, + }, + }, + }, + bloomfilters: { + type: "object", + name: "Bloom Filters (BIP37)", + description: "Setting for serving Bloom Filters", + spec: { + peerbloomfilters: { + type: "boolean", + name: "Serve Bloom Filters to Peers", + description: + "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", + warning: + "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", + default: false, + }, + }, + }, + }, + }, + }, + { + // convert this to `start-sdk/lib` for conversions + StartSdk: "./output.sdk", + }, +) diff --git a/sdk/package/lib/test/output.sdk.ts b/sdk/package/lib/test/output.sdk.ts new file mode 100644 index 000000000..3f3bb5411 --- /dev/null +++ b/sdk/package/lib/test/output.sdk.ts @@ -0,0 +1,53 @@ +import { CurrentDependenciesResult } from "../../../base/lib/dependencies/setupDependencies" +import { StartSdk } from "../StartSdk" +import { setupManifest } from "../manifest/setupManifest" +import { VersionGraph } from "../version/VersionGraph" + +export type Manifest = any +export const sdk = StartSdk.of() + .withManifest( + setupManifest({ + id: "testOutput", + title: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: { + main: { + source: { + dockerTag: "start9/hello-world", + }, + arch: ["aarch64", "x86_64"], + emulateMissingAs: "aarch64", + }, + }, + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + "remote-test": { + description: "", + optional: false, + s9pk: "https://example.com/remote-test.s9pk", + }, + }, + }), + ) + .withStore<{ storeRoot: { storeLeaf: "value" } }>() + .build(true) diff --git a/sdk/package/lib/test/output.test.ts b/sdk/package/lib/test/output.test.ts new file mode 100644 index 000000000..a2e9c35d5 --- /dev/null +++ b/sdk/package/lib/test/output.test.ts @@ -0,0 +1,146 @@ +import { InputSpecSpec, matchInputSpecSpec } from "./output" +import * as _I from "../index" +import { camelCase } from "../../scripts/oldSpecToBuilder" +import { deepMerge } from "../../../base/lib/util" + +export type IfEquals = + (() => G extends T ? 1 : 2) extends () => G extends U ? 1 : 2 ? Y : N +export function testOutput(): (c: IfEquals) => null { + return () => null +} + +/// Testing the types of the input spec +testOutput()(null) +testOutput()(null) +testOutput()(null) + +testOutput()(null) +testOutput< + InputSpecSpec["rpc"]["advanced"]["serialversion"], + "segwit" | "non-segwit" +>()(null) +testOutput()(null) +testOutput< + InputSpecSpec["advanced"]["peers"]["addnode"][0]["hostname"], + string | null +>()(null) +testOutput< + InputSpecSpec["testListUnion"][0]["union"]["value"]["name"], + string +>()(null) +testOutput()( + null, +) +testOutput>()( + null, +) + +// @ts-expect-error Because enable should be a boolean +testOutput()(null) +// prettier-ignore +// @ts-expect-error Expect that the string is the one above +testOutput()(null); + +/// Here we test the output of the matchInputSpecSpec function +describe("Inputs", () => { + const validInput: InputSpecSpec = { + mediasources: ["filebrowser"], + testListUnion: [ + { + union: { selection: "lnd", value: { name: "string" } }, + }, + ], + rpc: { + enable: true, + bio: "This is a bio", + username: "test", + password: "test", + advanced: { + auth: ["test"], + serialversion: "segwit", + servertimeout: 6, + threads: 3, + workqueue: 9, + }, + }, + "zmq-enabled": false, + txindex: false, + wallet: { enable: false, avoidpartialspends: false, discardfee: 0.0001 }, + advanced: { + mempool: { + maxmempool: 1, + persistmempool: true, + mempoolexpiry: 23, + mempoolfullrbf: true, + }, + peers: { + listen: true, + onlyconnect: true, + onlyonion: true, + addnode: [ + { + hostname: "test", + port: 1, + }, + ], + }, + dbcache: 5, + pruning: { + selection: "disabled", + value: { disabled: {} }, + }, + blockfilters: { + blockfilterindex: false, + peerblockfilters: false, + }, + bloomfilters: { peerbloomfilters: false }, + }, + } + + test("test valid input", () => { + const output = matchInputSpecSpec.unsafeCast(validInput) + expect(output).toEqual(validInput) + }) + test("test no longer care about the conversion of min/max and validating", () => { + matchInputSpecSpec.unsafeCast( + deepMerge({}, validInput, { rpc: { advanced: { threads: 0 } } }), + ) + }) + test("test errors should throw for number in string", () => { + expect(() => + matchInputSpecSpec.unsafeCast( + deepMerge({}, validInput, { rpc: { enable: 2 } }), + ), + ).toThrowError() + }) + test("Test that we set serialversion to something not segwit or non-segwit", () => { + expect(() => + matchInputSpecSpec.unsafeCast( + deepMerge({}, validInput, { + rpc: { advanced: { serialversion: "testing" } }, + }), + ), + ).toThrowError() + }) +}) + +describe("camelCase", () => { + test("'EquipmentClass name'", () => { + expect(camelCase("EquipmentClass name")).toEqual("equipmentClassName") + }) + test("'Equipment className'", () => { + expect(camelCase("Equipment className")).toEqual("equipmentClassName") + }) + test("'equipment class name'", () => { + expect(camelCase("equipment class name")).toEqual("equipmentClassName") + }) + test("'Equipment Class Name'", () => { + expect(camelCase("Equipment Class Name")).toEqual("equipmentClassName") + }) + test("'hyphen-name-format'", () => { + expect(camelCase("hyphen-name-format")).toEqual("hyphenNameFormat") + }) + test("'underscore_name_format'", () => { + expect(camelCase("underscore_name_format")).toEqual("underscoreNameFormat") + }) +}) diff --git a/sdk/package/lib/test/store.test.ts b/sdk/package/lib/test/store.test.ts new file mode 100644 index 000000000..f876e76d8 --- /dev/null +++ b/sdk/package/lib/test/store.test.ts @@ -0,0 +1,111 @@ +import { Effects } from "../../../base/lib/types" +import { extractJsonPath } from "../../../base/lib/util/PathBuilder" +import { StartSdk } from "../StartSdk" + +type Store = { + inputSpec: { + someValue: "a" | "b" + } +} +type Manifest = any +const todo = (): A => { + throw new Error("not implemented") +} +const noop = () => {} + +const sdk = StartSdk.of() + .withManifest({} as Manifest) + .withStore() + .build(true) + +const storePath = sdk.StorePath + +describe("Store", () => { + test("types", async () => { + ;async () => { + sdk.store.setOwn(todo(), storePath.inputSpec, { + someValue: "a", + }) + sdk.store.setOwn(todo(), storePath.inputSpec.someValue, "b") + sdk.store.setOwn(todo(), storePath, { + inputSpec: { someValue: "b" }, + }) + sdk.store.setOwn( + todo(), + storePath.inputSpec.someValue, + + // @ts-expect-error Type is wrong for the setting value + 5, + ) + sdk.store.setOwn( + todo(), + // @ts-expect-error Path is wrong + "/inputSpec/someVae3lue", + "someValue", + ) + + todo().store.set({ + path: extractJsonPath(storePath.inputSpec.someValue), + value: "b", + }) + todo().store.set({ + path: extractJsonPath(storePath.inputSpec.someValue), + //@ts-expect-error Path is wrong + value: "someValueIn", + }) + ;(await sdk.store + .getOwn(todo(), storePath.inputSpec.someValue) + .const()) satisfies string + ;(await sdk.store + .getOwn(todo(), storePath.inputSpec) + .const()) satisfies Store["inputSpec"] + await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), "/inputSpec/somdsfeValue") + .const() + /// ----------------- ERRORS ----------------- + + sdk.store.setOwn(todo(), storePath, { + // @ts-expect-error Type is wrong for the setting value + inputSpec: { someValue: "notInAOrB" }, + }) + sdk.store.setOwn( + todo(), + sdk.StorePath.inputSpec.someValue, + // @ts-expect-error Type is wrong for the setting value + "notInAOrB", + ) + ;(await sdk.store + .getOwn(todo(), storePath.inputSpec.someValue) + .const()) satisfies string + ;(await sdk.store + .getOwn(todo(), storePath.inputSpec) + .const()) satisfies Store["inputSpec"] + await sdk.store // @ts-expect-error Path is wrong + .getOwn("/inputSpec/somdsfeValue") + .const() + + /// + ;(await sdk.store + .getOwn(todo(), storePath.inputSpec.someValue) + // @ts-expect-error satisfies type is wrong + .const()) satisfies number + await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), extractJsonPath(storePath.inputSpec)) + .const() + ;(await todo().store.get({ + path: extractJsonPath(storePath.inputSpec.someValue), + callback: noop, + })) satisfies string + await todo().store.get({ + // @ts-expect-error Path is wrong as in it doesn't match above + path: "/inputSpec/someV2alue", + callback: noop, + }) + await todo().store.get({ + // @ts-expect-error Path is wrong as in it doesn't exists in wrapper type + path: "/inputSpec/someV2alue", + callback: noop, + }) + } + }) +}) diff --git a/sdk/package/lib/trigger/TriggerInput.ts b/sdk/package/lib/trigger/TriggerInput.ts new file mode 100644 index 000000000..e15cca9b7 --- /dev/null +++ b/sdk/package/lib/trigger/TriggerInput.ts @@ -0,0 +1,5 @@ +import { HealthStatus } from "../../../base/lib/types" + +export type TriggerInput = { + lastResult?: HealthStatus +} diff --git a/sdk/package/lib/trigger/changeOnFirstSuccess.ts b/sdk/package/lib/trigger/changeOnFirstSuccess.ts new file mode 100644 index 000000000..3da7284df --- /dev/null +++ b/sdk/package/lib/trigger/changeOnFirstSuccess.ts @@ -0,0 +1,32 @@ +import { Trigger } from "./index" + +export function changeOnFirstSuccess(o: { + beforeFirstSuccess: Trigger + afterFirstSuccess: Trigger +}): Trigger { + return async function* (getInput) { + let currentValue = getInput() + while (!currentValue.lastResult) { + yield + currentValue = getInput() + } + const beforeFirstSuccess = o.beforeFirstSuccess(getInput) + for ( + let res = await beforeFirstSuccess.next(); + currentValue?.lastResult !== "success" && !res.done; + res = await beforeFirstSuccess.next() + ) { + yield + currentValue = getInput() + } + const afterFirstSuccess = o.afterFirstSuccess(getInput) + for ( + let res = await afterFirstSuccess.next(); + !res.done; + res = await afterFirstSuccess.next() + ) { + yield + currentValue = getInput() + } + } +} diff --git a/sdk/package/lib/trigger/cooldownTrigger.ts b/sdk/package/lib/trigger/cooldownTrigger.ts new file mode 100644 index 000000000..991e81054 --- /dev/null +++ b/sdk/package/lib/trigger/cooldownTrigger.ts @@ -0,0 +1,8 @@ +export function cooldownTrigger(timeMs: number) { + return async function* () { + while (true) { + await new Promise((resolve) => setTimeout(resolve, timeMs)) + yield + } + } +} diff --git a/sdk/package/lib/trigger/defaultTrigger.ts b/sdk/package/lib/trigger/defaultTrigger.ts new file mode 100644 index 000000000..647695fb2 --- /dev/null +++ b/sdk/package/lib/trigger/defaultTrigger.ts @@ -0,0 +1,7 @@ +import { cooldownTrigger } from "./cooldownTrigger" +import { changeOnFirstSuccess } from "./changeOnFirstSuccess" + +export const defaultTrigger = changeOnFirstSuccess({ + beforeFirstSuccess: cooldownTrigger(1000), + afterFirstSuccess: cooldownTrigger(30000), +}) diff --git a/sdk/package/lib/trigger/index.ts b/sdk/package/lib/trigger/index.ts new file mode 100644 index 000000000..6da034262 --- /dev/null +++ b/sdk/package/lib/trigger/index.ts @@ -0,0 +1,7 @@ +import { TriggerInput } from "./TriggerInput" +export { changeOnFirstSuccess } from "./changeOnFirstSuccess" +export { cooldownTrigger } from "./cooldownTrigger" + +export type Trigger = ( + getInput: () => TriggerInput, +) => AsyncIterator diff --git a/sdk/package/lib/trigger/lastStatus.ts b/sdk/package/lib/trigger/lastStatus.ts new file mode 100644 index 000000000..01e737314 --- /dev/null +++ b/sdk/package/lib/trigger/lastStatus.ts @@ -0,0 +1,33 @@ +import { Trigger } from "." +import { HealthStatus } from "../../../base/lib/types" + +export type LastStatusTriggerParams = { [k in HealthStatus]?: Trigger } & { + default: Trigger +} + +export function lastStatus(o: LastStatusTriggerParams): Trigger { + return async function* (getInput) { + let trigger = o.default(getInput) + const triggers: { + [k in HealthStatus]?: AsyncIterator + } & { default: AsyncIterator } = { + default: trigger, + } + while (true) { + let currentValue = getInput() + let prev: HealthStatus | "default" | undefined = currentValue.lastResult + if (!prev) { + yield + continue + } + if (!(prev in o)) { + prev = "default" + } + if (!triggers[prev]) { + triggers[prev] = o[prev]!(getInput) + } + await triggers[prev]?.next() + yield + } + } +} diff --git a/sdk/package/lib/trigger/successFailure.ts b/sdk/package/lib/trigger/successFailure.ts new file mode 100644 index 000000000..7febcd356 --- /dev/null +++ b/sdk/package/lib/trigger/successFailure.ts @@ -0,0 +1,7 @@ +import { Trigger } from "." +import { lastStatus } from "./lastStatus" + +export const successFailure = (o: { + duringSuccess: Trigger + duringError: Trigger +}) => lastStatus({ success: o.duringSuccess, default: o.duringError }) diff --git a/sdk/package/lib/util/GetSslCertificate.ts b/sdk/package/lib/util/GetSslCertificate.ts new file mode 100644 index 000000000..72bf9e22d --- /dev/null +++ b/sdk/package/lib/util/GetSslCertificate.ts @@ -0,0 +1,47 @@ +import { T } from ".." +import { Effects } from "../../../base/lib/Effects" + +export class GetSslCertificate { + constructor( + readonly effects: Effects, + readonly hostnames: string[], + readonly algorithm?: T.Algorithm, + ) {} + + /** + * Returns the system SMTP credentials. Restarts the service if the credentials change + */ + const() { + return this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + callback: () => this.effects.constRetry(), + }) + } + /** + * Returns the system SMTP credentials. Does nothing if the credentials change + */ + once() { + return this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + }) + } + /** + * Watches the system SMTP credentials. Takes a custom callback function to run whenever the credentials change + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + callback: () => callback(), + }) + await waitForNext + } + } +} diff --git a/sdk/package/lib/util/SubContainer.ts b/sdk/package/lib/util/SubContainer.ts new file mode 100644 index 000000000..77b31fec8 --- /dev/null +++ b/sdk/package/lib/util/SubContainer.ts @@ -0,0 +1,440 @@ +import * as fs from "fs/promises" +import * as T from "../../../base/lib/types" +import * as cp from "child_process" +import { promisify } from "util" +import { Buffer } from "node:buffer" +import { once } from "../../../base/lib/util/once" + +export const execFile = promisify(cp.execFile) +const False = () => false + +type ExecResults = { + exitCode: number | null + exitSignal: NodeJS.Signals | null + stdout: string | Buffer + stderr: string | Buffer +} + +export type ExecOptions = { + input?: string | Buffer +} + +const TIMES_TO_WAIT_FOR_PROC = 100 + +/** + * This is the type that is going to describe what an subcontainer could do. The main point of the + * subcontainer is to have commands that run in a chrooted environment. This is useful for running + * commands in a containerized environment. But, I wanted the destroy to sometimes be doable, for example the + * case where the subcontainer isn't owned by the process, the subcontainer shouldn't be destroyed. + */ +export interface ExecSpawnable { + get destroy(): undefined | (() => Promise) + exec( + command: string[], + options?: CommandOptions & ExecOptions, + timeoutMs?: number | null, + ): Promise + spawn( + command: string[], + options?: CommandOptions & StdioOptions, + ): Promise +} +/** + * Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts. + * + * Implements: + * @see {@link ExecSpawnable} + */ +export class SubContainer implements ExecSpawnable { + private leader: cp.ChildProcess + private leaderExited: boolean = false + private waitProc: () => Promise + private constructor( + readonly effects: T.Effects, + readonly imageId: T.ImageId, + readonly rootfs: string, + readonly guid: T.Guid, + ) { + this.leaderExited = false + this.leader = cp.spawn("start-cli", ["subcontainer", "launch", rootfs], { + killSignal: "SIGKILL", + stdio: "inherit", + }) + this.leader.on("exit", () => { + this.leaderExited = true + }) + this.waitProc = once( + () => + new Promise(async (resolve, reject) => { + let count = 0 + while ( + !(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False)) + ) { + if (count++ > TIMES_TO_WAIT_FOR_PROC) { + console.debug("Failed to start subcontainer", { + guid: this.guid, + imageId: this.imageId, + rootfs: this.rootfs, + }) + reject(new Error(`Failed to start subcontainer ${this.imageId}`)) + } + await wait(1) + } + resolve(null) + }), + ) + } + static async of( + effects: T.Effects, + image: { imageId: T.ImageId; sharedRun?: boolean }, + name: string, + ) { + const { imageId, sharedRun } = image + const [rootfs, guid] = await effects.subcontainer.createFs({ + imageId, + name, + }) + + const shared = ["dev", "sys"] + if (!!sharedRun) { + shared.push("run") + } + + await fs.mkdir(`${rootfs}/etc`, { recursive: true }) + await fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`) + + for (const dirPart of shared) { + const from = `/${dirPart}` + const to = `${rootfs}/${dirPart}` + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(to, { recursive: true }) + await execFile("mount", ["--rbind", from, to]) + } + + return new SubContainer(effects, imageId, rootfs, guid) + } + + static async with( + effects: T.Effects, + image: { imageId: T.ImageId; sharedRun?: boolean }, + mounts: { options: MountOptions; path: string }[], + name: string, + fn: (subContainer: SubContainer) => Promise, + ): Promise { + const subContainer = await SubContainer.of(effects, image, name) + try { + for (let mount of mounts) { + await subContainer.mount(mount.options, mount.path) + } + return await fn(subContainer) + } finally { + await subContainer.destroy() + } + } + + async mount(options: MountOptions, path: string): Promise { + path = path.startsWith("/") + ? `${this.rootfs}${path}` + : `${this.rootfs}/${path}` + if (options.type === "volume") { + const subpath = options.subpath + ? options.subpath.startsWith("/") + ? options.subpath + : `/${options.subpath}` + : "/" + const from = `/media/startos/volumes/${options.id}${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await execFile("mount", ["--bind", from, path]) + } else if (options.type === "assets") { + const subpath = options.subpath + ? options.subpath.startsWith("/") + ? options.subpath + : `/${options.subpath}` + : "/" + const from = `/media/startos/assets/${options.id}${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await execFile("mount", ["--bind", from, path]) + } else if (options.type === "pointer") { + await this.effects.mount({ location: path, target: options }) + } else if (options.type === "backup") { + const subpath = options.subpath + ? options.subpath.startsWith("/") + ? options.subpath + : `/${options.subpath}` + : "/" + const from = `/media/startos/backup${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await execFile("mount", ["--bind", from, path]) + } else { + throw new Error(`unknown type ${(options as any).type}`) + } + return this + } + + private async killLeader() { + if (this.leaderExited) { + return + } + return new Promise((resolve, reject) => { + try { + let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000) + this.leader.on("exit", () => { + clearTimeout(timeout) + resolve(null) + }) + if (!this.leader.kill("SIGTERM")) { + reject(new Error("kill(2) failed")) + } + } catch (e) { + reject(e) + } + }) + } + + get destroy() { + return async () => { + const guid = this.guid + await this.killLeader() + await this.effects.subcontainer.destroyFs({ guid }) + return null + } + } + + async exec( + command: string[], + options?: CommandOptions & ExecOptions, + timeoutMs: number | null = 30000, + ): Promise<{ + exitCode: number | null + exitSignal: NodeJS.Signals | null + stdout: string | Buffer + stderr: string | Buffer + }> { + await this.waitProc() + const imageMeta: T.ImageMetadata = await fs + .readFile(`/media/startos/images/${this.imageId}.json`, { + encoding: "utf8", + }) + .catch(() => "{}") + .then(JSON.parse) + let extra: string[] = [] + if (options?.user) { + extra.push(`--user=${options.user}`) + delete options.user + } + let workdir = imageMeta.workdir || "/" + if (options?.cwd) { + workdir = options.cwd + delete options.cwd + } + const child = cp.spawn( + "start-cli", + [ + "subcontainer", + "exec", + `--env=/media/startos/images/${this.imageId}.env`, + `--workdir=${workdir}`, + ...extra, + this.rootfs, + ...command, + ], + options || {}, + ) + if (options?.input) { + await new Promise((resolve, reject) => + child.stdin.write(options.input, (e) => { + if (e) { + reject(e) + } else { + resolve(null) + } + }), + ) + await new Promise((resolve) => child.stdin.end(resolve)) + } + const pid = child.pid + const stdout = { data: "" as string } + const stderr = { data: "" as string } + const appendData = + (appendTo: { data: string }) => (chunk: string | Buffer | any) => { + if (typeof chunk === "string" || chunk instanceof Buffer) { + appendTo.data += chunk.toString() + } else { + console.error("received unexpected chunk", chunk) + } + } + return new Promise((resolve, reject) => { + child.on("error", reject) + let killTimeout: NodeJS.Timeout | undefined + if (timeoutMs !== null && child.pid) { + killTimeout = setTimeout(() => child.kill("SIGKILL"), timeoutMs) + } + child.stdout.on("data", appendData(stdout)) + child.stderr.on("data", appendData(stderr)) + child.on("exit", (code, signal) => { + clearTimeout(killTimeout) + resolve({ + exitCode: code, + exitSignal: signal, + stdout: stdout.data, + stderr: stderr.data, + }) + }) + }) + } + + async launch( + command: string[], + options?: CommandOptions, + ): Promise { + await this.waitProc() + const imageMeta: any = await fs + .readFile(`/media/startos/images/${this.imageId}.json`, { + encoding: "utf8", + }) + .catch(() => "{}") + .then(JSON.parse) + let extra: string[] = [] + if (options?.user) { + extra.push(`--user=${options.user}`) + delete options.user + } + let workdir = imageMeta.workdir || "/" + if (options?.cwd) { + workdir = options.cwd + delete options.cwd + } + await this.killLeader() + this.leaderExited = false + this.leader = cp.spawn( + "start-cli", + [ + "subcontainer", + "launch", + `--env=/media/startos/images/${this.imageId}.env`, + `--workdir=${workdir}`, + ...extra, + this.rootfs, + ...command, + ], + { ...options, stdio: "inherit" }, + ) + this.leader.on("exit", () => { + this.leaderExited = true + }) + return this.leader as cp.ChildProcessWithoutNullStreams + } + + async spawn( + command: string[], + options: CommandOptions & StdioOptions = { stdio: "inherit" }, + ): Promise { + await this.waitProc() + const imageMeta: any = await fs + .readFile(`/media/startos/images/${this.imageId}.json`, { + encoding: "utf8", + }) + .catch(() => "{}") + .then(JSON.parse) + let extra: string[] = [] + if (options.user) { + extra.push(`--user=${options.user}`) + delete options.user + } + let workdir = imageMeta.workdir || "/" + if (options.cwd) { + workdir = options.cwd + delete options.cwd + } + return cp.spawn( + "start-cli", + [ + "subcontainer", + "exec", + `--env=/media/startos/images/${this.imageId}.env`, + `--workdir=${workdir}`, + ...extra, + this.rootfs, + ...command, + ], + options, + ) + } +} + +/** + * Take an subcontainer but remove the ability to add the mounts and the destroy function. + * Lets other functions, like health checks, to not destroy the parents. + * + */ +export class SubContainerHandle implements ExecSpawnable { + constructor(private subContainer: ExecSpawnable) {} + get destroy() { + return undefined + } + + exec( + command: string[], + options?: CommandOptions, + timeoutMs?: number | null, + ): Promise { + return this.subContainer.exec(command, options, timeoutMs) + } + spawn( + command: string[], + options: CommandOptions & StdioOptions = { stdio: "inherit" }, + ): Promise { + return this.subContainer.spawn(command, options) + } +} + +export type CommandOptions = { + env?: { [variable: string]: string } + cwd?: string + user?: string +} + +export type StdioOptions = { + stdio?: cp.IOType +} + +export type MountOptions = + | MountOptionsVolume + | MountOptionsAssets + | MountOptionsPointer + | MountOptionsBackup + +export type MountOptionsVolume = { + type: "volume" + id: string + subpath: string | null + readonly: boolean +} + +export type MountOptionsAssets = { + type: "assets" + id: string + subpath: string | null +} + +export type MountOptionsPointer = { + type: "pointer" + packageId: string + volumeId: string + subpath: string | null + readonly: boolean +} + +export type MountOptionsBackup = { + type: "backup" + subpath: string | null +} +function wait(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)) +} diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts new file mode 100644 index 000000000..d47af510c --- /dev/null +++ b/sdk/package/lib/util/fileHelper.ts @@ -0,0 +1,253 @@ +import * as matches from "ts-matches" +import * as YAML from "yaml" +import * as TOML from "@iarna/toml" +import merge from "lodash.merge" +import * as T from "../../../base/lib/types" +import * as fs from "node:fs/promises" +import { asError } from "../../../base/lib/util" + +const previousPath = /(.+?)\/([^/]*)$/ + +const exists = (path: string) => + fs.access(path).then( + () => true, + () => false, + ) + +async function onCreated(path: string) { + if (path === "/") return + if (!path.startsWith("/")) path = `${process.cwd()}/${path}` + if (await exists(path)) { + return + } + const split = path.split("/") + const filename = split.pop() + const parent = split.join("/") + await onCreated(parent) + const ctrl = new AbortController() + const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal }) + if ( + await fs.access(path).then( + () => true, + () => false, + ) + ) { + ctrl.abort("finished") + return + } + for await (let event of watch) { + if (event.filename === filename) { + ctrl.abort("finished") + return + } + } +} + +/** + * @description Use this class to read/write an underlying configuration file belonging to the upstream service. + * + * These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it. + * + * It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type. + * + * Officially supported file types are json, yaml, and toml. Other files types can use "raw" + * + * Choose between officially supported file formats (), or a custom format (raw). + * + * @example + * Below are a few examples + * + * ``` + * import { matches, FileHelper } from '@start9labs/start-sdk' + * const { arrayOf, boolean, literal, literals, object, natural, string } = matches + * + * export const jsonFile = FileHelper.json('./inputSpec.json', object({ + * passwords: arrayOf(string).onMismatch([]) + * type: literals('private', 'public').optional().onMismatch(undefined) + * })) + * + * export const tomlFile = FileHelper.toml('./inputSpec.toml', object({ + * url: literal('https://start9.com').onMismatch('https://start9.com') + * public: boolean.onMismatch(true) + * })) + * + * export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({ + * name: string.optional().onMismatch(undefined) + * age: natural.optional().onMismatch(undefined) + * })) + * + * export const bitcoinConfFile = FileHelper.raw( + * './service.conf', + * (obj: CustomType) => customConvertObjToFormattedString(obj), + * (str) => customParseStringToTypedObj(str), + * ) + * ``` + */ +export class FileHelper { + protected constructor( + readonly path: string, + readonly writeData: (dataIn: A) => string, + readonly readData: (stringValue: string) => unknown, + readonly validate: (value: unknown) => A, + ) {} + + /** + * Accepts structured data and overwrites the existing file on disk. + */ + private async writeFile(data: A): Promise { + const parent = previousPath.exec(this.path) + if (parent) { + await fs.mkdir(parent[1], { recursive: true }) + } + + await fs.writeFile(this.path, this.writeData(data)) + + return null + } + + private async readFile(): Promise { + if (!(await exists(this.path))) { + return null + } + return this.readData( + await fs.readFile(this.path).then((data) => data.toString("utf-8")), + ) + } + + /** + * Reads the file from disk and converts it to structured data. + */ + private async readOnce(): Promise { + const data = await this.readFile() + if (!data) return null + return this.validate(data) + } + + private async readConst(effects: T.Effects): Promise { + const watch = this.readWatch() + const res = await watch.next() + watch.next().then(effects.constRetry) + return res.value + } + + private async *readWatch() { + let res + while (true) { + if (await exists(this.path)) { + const ctrl = new AbortController() + const watch = fs.watch(this.path, { + persistent: false, + signal: ctrl.signal, + }) + res = await this.readOnce() + const listen = Promise.resolve() + .then(async () => { + for await (const _ of watch) { + ctrl.abort("finished") + return null + } + }) + .catch((e) => console.error(asError(e))) + yield res + await listen + } else { + yield null + await onCreated(this.path).catch((e) => console.error(asError(e))) + } + } + return null + } + + get read() { + return { + once: () => this.readOnce(), + const: (effects: T.Effects) => this.readConst(effects), + watch: () => this.readWatch(), + } + } + + /** + * Accepts full structured data and performs a merge with the existing file on disk if it exists. + */ + async write(data: A) { + const fileData = (await this.readFile()) || {} + const mergeData = merge({}, fileData, data) + return await this.writeFile(this.validate(mergeData)) + } + + /** + * Accepts partial structured data and performs a merge with the existing file on disk. + */ + async merge(data: T.DeepPartial) { + const fileData = + (await this.readFile()) || + (() => { + throw new Error(`${this.path}: does not exist`) + })() + const mergeData = merge({}, fileData, data) + return await this.writeFile(this.validate(mergeData)) + } + + /** + * We wanted to be able to have a fileHelper, and just modify the path later in time. + * Like one behavior of another dependency or something similar. + */ + withPath(path: string) { + return new FileHelper(path, this.writeData, this.readData, this.validate) + } + + /** + * Create a File Helper for an arbitrary file type. + * + * Provide custom functions for translating data to/from the file format. + */ + static raw( + path: string, + toFile: (dataIn: A) => string, + fromFile: (rawData: string) => unknown, + validate: (data: unknown) => A, + ) { + return new FileHelper(path, toFile, fromFile, validate) + } + /** + * Create a File Helper for a .json file. + */ + static json(path: string, shape: matches.Validator) { + return new FileHelper( + path, + (inData) => JSON.stringify(inData, null, 2), + (inString) => JSON.parse(inString), + (data) => shape.unsafeCast(data), + ) + } + /** + * Create a File Helper for a .toml file + */ + static toml>( + path: string, + shape: matches.Validator, + ) { + return new FileHelper( + path, + (inData) => TOML.stringify(inData as any), + (inString) => TOML.parse(inString), + (data) => shape.unsafeCast(data), + ) + } + /** + * Create a File Helper for a .yaml file + */ + static yaml>( + path: string, + shape: matches.Validator, + ) { + return new FileHelper( + path, + (inData) => YAML.stringify(inData, null, 2), + (inString) => YAML.parse(inString), + (data) => shape.unsafeCast(data), + ) + } +} + +export default FileHelper diff --git a/sdk/package/lib/util/index.ts b/sdk/package/lib/util/index.ts new file mode 100644 index 000000000..66c73503e --- /dev/null +++ b/sdk/package/lib/util/index.ts @@ -0,0 +1,4 @@ +export * from "../../../base/lib/util" +export { GetSslCertificate } from "./GetSslCertificate" + +export { hostnameInfoToAddress } from "../../../base/lib/util/Hostname" diff --git a/sdk/package/lib/version/VersionGraph.ts b/sdk/package/lib/version/VersionGraph.ts new file mode 100644 index 000000000..91c7a0cc0 --- /dev/null +++ b/sdk/package/lib/version/VersionGraph.ts @@ -0,0 +1,203 @@ +import { ExtendedVersion, VersionRange } from "../../../base/lib/exver" +import * as T from "../../../base/lib/types" +import { Graph, Vertex, once } from "../util" +import { IMPOSSIBLE, VersionInfo } from "./VersionInfo" + +export class VersionGraph { + private readonly graph: () => Graph< + ExtendedVersion | VersionRange, + ((opts: { effects: T.Effects }) => Promise) | undefined + > + private constructor( + readonly current: VersionInfo, + versions: Array>, + ) { + this.graph = once(() => { + const graph = new Graph< + ExtendedVersion | VersionRange, + ((opts: { effects: T.Effects }) => Promise) | undefined + >() + const flavorMap: Record< + string, + [ + ExtendedVersion, + VersionInfo, + Vertex< + ExtendedVersion | VersionRange, + ((opts: { effects: T.Effects }) => Promise) | undefined + >, + ][] + > = {} + for (let version of [current, ...versions]) { + const v = ExtendedVersion.parse(version.options.version) + const vertex = graph.addVertex(v, [], []) + const flavor = v.flavor || "" + if (!flavorMap[flavor]) { + flavorMap[flavor] = [] + } + flavorMap[flavor].push([v, version, vertex]) + } + for (let flavor in flavorMap) { + flavorMap[flavor].sort((a, b) => a[0].compareForSort(b[0])) + let prev: + | [ + ExtendedVersion, + VersionInfo, + Vertex< + ExtendedVersion | VersionRange, + (opts: { effects: T.Effects }) => Promise + >, + ] + | undefined = undefined + for (let [v, version, vertex] of flavorMap[flavor]) { + if (version.options.migrations.up !== IMPOSSIBLE) { + let range + if (prev) { + graph.addEdge(version.options.migrations.up, prev[2], vertex) + range = VersionRange.anchor(">=", prev[0]).and( + VersionRange.anchor("<", v), + ) + } else { + range = VersionRange.anchor("<", v) + } + const vRange = graph.addVertex(range, [], []) + graph.addEdge(version.options.migrations.up, vRange, vertex) + } + + if (version.options.migrations.down !== IMPOSSIBLE) { + let range + if (prev) { + graph.addEdge(version.options.migrations.down, vertex, prev[2]) + range = VersionRange.anchor(">=", prev[0]).and( + VersionRange.anchor("<", v), + ) + } else { + range = VersionRange.anchor("<", v) + } + const vRange = graph.addVertex(range, [], []) + graph.addEdge(version.options.migrations.down, vertex, vRange) + } + + if (version.options.migrations.other) { + for (let rangeStr in version.options.migrations.other) { + const range = VersionRange.parse(rangeStr) + const vRange = graph.addVertex(range, [], []) + graph.addEdge( + version.options.migrations.other[rangeStr], + vRange, + vertex, + ) + for (let matching of graph.findVertex( + (v) => + v.metadata instanceof ExtendedVersion && + v.metadata.satisfies(range), + )) { + graph.addEdge( + version.options.migrations.other[rangeStr], + matching, + vertex, + ) + } + } + } + } + } + return graph + }) + } + currentVersion = once(() => + ExtendedVersion.parse(this.current.options.version), + ) + /** + * Each exported `VersionInfo.of()` should be imported and provided as an argument to this function. + * + * ** The current version must be the FIRST argument. ** + */ + static of< + CurrentVersion extends string, + OtherVersions extends Array>, + >( + currentVersion: VersionInfo, + ...other: EnsureUniqueId + ) { + return new VersionGraph(currentVersion, other as Array>) + } + async migrate({ + effects, + from, + to, + }: { + effects: T.Effects + from: ExtendedVersion + to: ExtendedVersion + }) { + const graph = this.graph() + if (from && to) { + const path = graph.shortestPath( + (v) => + (v.metadata instanceof VersionRange && + v.metadata.satisfiedBy(from)) || + (v.metadata instanceof ExtendedVersion && v.metadata.equals(from)), + (v) => + (v.metadata instanceof VersionRange && v.metadata.satisfiedBy(to)) || + (v.metadata instanceof ExtendedVersion && v.metadata.equals(to)), + ) + if (path) { + for (let edge of path) { + if (edge.metadata) { + await edge.metadata({ effects }) + } + await effects.setDataVersion({ version: edge.to.metadata.toString() }) + } + return + } + } + throw new Error() + } + canMigrateFrom = once(() => + Array.from( + this.graph().reverseBreadthFirstSearch( + (v) => + (v.metadata instanceof VersionRange && + v.metadata.satisfiedBy(this.currentVersion())) || + (v.metadata instanceof ExtendedVersion && + v.metadata.equals(this.currentVersion())), + ), + ).reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ), + ) + canMigrateTo = once(() => + Array.from( + this.graph().breadthFirstSearch( + (v) => + (v.metadata instanceof VersionRange && + v.metadata.satisfiedBy(this.currentVersion())) || + (v.metadata instanceof ExtendedVersion && + v.metadata.equals(this.currentVersion())), + ), + ).reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ), + ) +} + +// prettier-ignore +export type EnsureUniqueId = + B extends [] ? A : + B extends [VersionInfo, ...infer Rest] ? ( + Version extends OtherVersions ? "One or more versions are not unique"[] : + EnsureUniqueId + ) : "There exists a migration that is not a Migration"[] diff --git a/sdk/package/lib/version/VersionInfo.ts b/sdk/package/lib/version/VersionInfo.ts new file mode 100644 index 000000000..952ae5352 --- /dev/null +++ b/sdk/package/lib/version/VersionInfo.ts @@ -0,0 +1,83 @@ +import { ValidateExVer } from "../../../base/lib/exver" +import * as T from "../../../base/lib/types" + +export const IMPOSSIBLE = Symbol("IMPOSSIBLE") + +export type VersionOptions = { + /** The exver-compliant version number */ + version: Version & ValidateExVer + /** The release notes for this version */ + releaseNotes: string + /** Data migrations for this version */ + migrations: { + /** + * A migration from the previous version. Leave empty to indicate no migration is necessary. + * Set to `IMPOSSIBLE` to indicate migrating from the previous version is not possible. + */ + up?: ((opts: { effects: T.Effects }) => Promise) | typeof IMPOSSIBLE + /** + * A migration to the previous version. Leave blank to indicate no migration is necessary. + * Set to `IMPOSSIBLE` to indicate downgrades are prohibited + */ + down?: ((opts: { effects: T.Effects }) => Promise) | typeof IMPOSSIBLE + /** + * Additional migrations, such as fast-forward migrations, or migrations from other flavors. + */ + other?: Record Promise> + } +} + +export class VersionInfo { + private _version: null | Version = null + private constructor( + readonly options: VersionOptions & { satisfies: string[] }, + ) {} + /** + * @description Use this function to define a new version of the service. By convention, each version should receive its own file. + * @property {string} version + * @property {string} releaseNotes + * @property {object} migrations + * @returns A VersionInfo class instance that is exported, then imported into versions/index.ts. + */ + static of(options: VersionOptions) { + return new VersionInfo({ ...options, satisfies: [] }) + } + /** Specify a version that this version is 100% backwards compatible to */ + satisfies( + version: V & ValidateExVer, + ): VersionInfo { + return new VersionInfo({ + ...this.options, + satisfies: [...this.options.satisfies, version], + }) + } +} + +function __type_tests() { + const version: VersionInfo<"1.0.0:0"> = VersionInfo.of({ + version: "1.0.0:0", + releaseNotes: "", + migrations: {}, + }) + .satisfies("#other:1.0.0:0") + .satisfies("#other:2.0.0:0") + // @ts-expect-error + .satisfies("#other:2.f.0:0") + + let a: VersionInfo<"1.0.0:0"> = version + // @ts-expect-error + let b: VersionInfo<"1.0.0:3"> = version + + VersionInfo.of({ + // @ts-expect-error + version: "test", + releaseNotes: "", + migrations: {}, + }) + VersionInfo.of({ + // @ts-expect-error + version: "test" as string, + releaseNotes: "", + migrations: {}, + }) +} diff --git a/sdk/package/lib/version/index.ts b/sdk/package/lib/version/index.ts new file mode 100644 index 000000000..c7a47fc38 --- /dev/null +++ b/sdk/package/lib/version/index.ts @@ -0,0 +1,2 @@ +export * from "./VersionGraph" +export * from "./VersionInfo" diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json new file mode 100644 index 000000000..6f2cf74d8 --- /dev/null +++ b/sdk/package/package-lock.json @@ -0,0 +1,4714 @@ +{ + "name": "@start9labs/start-sdk", + "version": "0.3.6-beta.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@start9labs/start-sdk", + "version": "0.3.6-beta.4", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } + }, + "../base": { + "name": "@start9labs/start-sdk-base", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } + }, + "../base/dist": { + "extraneous": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", + "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz", + "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.21.3", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.3", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.3", + "@babel/types": "^7.21.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", + "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", + "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", + "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", + "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", + "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.21.3", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.3", + "@babel/types": "^7.21.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", + "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "dependencies": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "18.15.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", + "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==", + "dev": true + }, + "node_modules/@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001470", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001470.tgz", + "integrity": "sha512-065uNwY6QtHCBOExzbV6m236DDhYCCtPmQUCoQtwkVqzud8v5QPidoMr6CoMkC2nfp6nksjttqWQRRh75LqUmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.341", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.341.tgz", + "integrity": "sha512-R4A8VfUBQY9WmAhuqY5tjHRf5fH2AAf6vqitBOE0y6u2PgHgqHSrhZmu78dIX3fVZtjqlwJNX1i2zwC3VpHtQQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-fetch/node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/peggy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-3.0.2.tgz", + "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", + "dev": true, + "dependencies": { + "commander": "^10.0.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", + "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-jest": { + "version": "29.0.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", + "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-matches": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", + "license": "MIT" + }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-pegjs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ts-pegjs/-/ts-pegjs-4.2.1.tgz", + "integrity": "sha512-mK/O2pu6lzWUeKpEMA/wsa0GdYblfjJI1y0s0GqH6xCTvugQDOWPJbm5rY6AHivpZICuXIriCb+a7Cflbdtc2w==", + "dev": true, + "dependencies": { + "prettier": "^2.8.8", + "ts-morph": "^18.0.0" + }, + "bin": { + "tspegjs": "dist/cli.mjs" + }, + "peerDependencies": { + "peggy": "^3.0.2" + } + }, + "node_modules/ts-pegjs/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", + "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", + "dev": true, + "dependencies": { + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sdk/package/package.json b/sdk/package/package.json new file mode 100644 index 000000000..5e80f5a94 --- /dev/null +++ b/sdk/package/package.json @@ -0,0 +1,60 @@ +{ + "name": "@start9labs/start-sdk", + "version": "0.3.6-beta.4", + "description": "Software development kit to facilitate packaging services for StartOS", + "main": "./package/lib/index.js", + "types": "./package/lib/index.d.ts", + "sideEffects": true, + "typesVersion": { + ">=3.1": { + "*": [ + "package/lib/*", + "base/lib/*" + ] + } + }, + "scripts": { + "test": "jest -c ./jest.config.js --coverage", + "buildOutput": "ts-node ./lib/test/makeOutput.ts && npx prettier --write \"**/*.ts\"", + "check": "tsc --noEmit", + "tsc": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Start9Labs/start-sdk.git" + }, + "author": "Start9 Labs", + "license": "MIT", + "bugs": { + "url": "https://github.com/Start9Labs/start-sdk/issues" + }, + "homepage": "https://github.com/Start9Labs/start-sdk#readme", + "dependencies": { + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2", + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": false + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" + } +} diff --git a/sdk/package/scripts/oldSpecToBuilder.ts b/sdk/package/scripts/oldSpecToBuilder.ts new file mode 100644 index 000000000..11ef30340 --- /dev/null +++ b/sdk/package/scripts/oldSpecToBuilder.ts @@ -0,0 +1,386 @@ +import * as fs from "fs" + +// https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case +export function camelCase(value: string) { + return value + .replace(/([\(\)\[\]])/g, "") + .replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) { + if (p2) return p2.toUpperCase() + return p1.toLowerCase() + }) +} + +export async function oldSpecToBuilder( + file: string, + inputData: Promise | any, + options?: Parameters[1], +) { + await fs.writeFile( + file, + await makeFileContentFromOld(inputData, options), + (err) => console.error(err), + ) +} + +function isString(x: unknown): x is string { + return typeof x === "string" +} + +export default async function makeFileContentFromOld( + inputData: Promise | any, + { StartSdk = "start-sdk", nested = true } = {}, +) { + const outputLines: string[] = [] + outputLines.push(` +import { sdk } from "${StartSdk}" +const {InputSpec, List, Value, Variants} = sdk +`) + const data = await inputData + + const namedConsts = new Set(["InputSpec", "Value", "List"]) + const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data)) + const inputSpecMatcherName = newConst( + "matchInputSpecSpec", + `${inputSpecName}.validator`, + ) + outputLines.push( + `export type InputSpecSpec = typeof ${inputSpecMatcherName}._TYPE;`, + ) + + return outputLines.join("\n") + + function newConst(key: string, data: string, type?: string) { + const variableName = getNextConstName(camelCase(key)) + outputLines.push( + `export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`, + ) + return variableName + } + function maybeNewConst(key: string, data: string) { + if (nested) return data + return newConst(key, data) + } + function convertInputSpecInner(data: any) { + let answer = "{" + for (const [key, value] of Object.entries(data)) { + const variableName = maybeNewConst(key, convertValueSpec(value)) + + answer += `${JSON.stringify(key)}: ${variableName},` + } + return `${answer}}` + } + + function convertInputSpec(data: any) { + return `InputSpec.of(${convertInputSpecInner(data)})` + } + function convertValueSpec(value: any): string { + switch (value.type) { + case "string": { + if (value.textarea) { + return `${rangeToTodoComment( + value?.range, + )}Value.textarea(${JSON.stringify( + { + name: value.name || null, + description: value.description || null, + warning: value.warning || null, + required: !(value.nullable || false), + default: value.default, + placeholder: value.placeholder || null, + maxLength: null, + minLength: null, + }, + null, + 2, + )})` + } + return `${rangeToTodoComment(value?.range)}Value.text(${JSON.stringify( + { + name: value.name || null, + default: value.default || null, + required: !value.nullable, + description: value.description || null, + warning: value.warning || null, + masked: value.masked || false, + placeholder: value.placeholder || null, + inputmode: "text", + patterns: value.pattern + ? [ + { + regex: value.pattern, + description: value["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + }, + null, + 2, + )})` + } + case "number": { + return `${rangeToTodoComment( + value?.range, + )}Value.number(${JSON.stringify( + { + name: value.name || null, + description: value.description || null, + warning: value.warning || null, + default: value.default || null, + required: !value.nullable, + min: null, + max: null, + step: null, + integer: value.integral || false, + units: value.units || null, + placeholder: value.placeholder || null, + }, + null, + 2, + )})` + } + case "boolean": { + return `Value.toggle(${JSON.stringify( + { + name: value.name || null, + default: value.default || false, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2, + )})` + } + case "enum": { + const allValueNames = new Set([ + ...(value?.["values"] || []), + ...Object.keys(value?.["value-names"] || {}), + ]) + const values = Object.fromEntries( + Array.from(allValueNames) + .filter(isString) + .map((key) => [key, value?.spec?.["value-names"]?.[key] || key]), + ) + return `Value.select(${JSON.stringify( + { + name: value.name || null, + description: value.description || null, + warning: value.warning || null, + default: value.default, + values, + }, + null, + 2, + )} as const)` + } + case "object": { + const specName = maybeNewConst( + value.name + "_spec", + convertInputSpec(value.spec), + ) + return `Value.object({ + name: ${JSON.stringify(value.name || null)}, + description: ${JSON.stringify(value.description || null)}, + }, ${specName})` + } + case "union": { + const variants = maybeNewConst( + value.name + "_variants", + convertVariants(value.variants, value.tag["variant-names"] || {}), + ) + + return `Value.union({ + name: ${JSON.stringify(value.name || null)}, + description: ${JSON.stringify(value.tag.description || null)}, + warning: ${JSON.stringify(value.tag.warning || null)}, + default: ${JSON.stringify(value.default)}, + }, ${variants})` + } + case "list": { + if (value.subtype === "enum") { + const allValueNames = new Set([ + ...(value?.spec?.["values"] || []), + ...Object.keys(value?.spec?.["value-names"] || {}), + ]) + const values = Object.fromEntries( + Array.from(allValueNames) + .filter(isString) + .map((key: string) => [ + key, + value?.spec?.["value-names"]?.[key] ?? key, + ]), + ) + return `Value.multiselect(${JSON.stringify( + { + name: value.name || null, + minLength: null, + maxLength: null, + default: value.default ?? null, + description: value.description || null, + warning: value.warning || null, + values, + }, + null, + 2, + )})` + } + const list = maybeNewConst(value.name + "_list", convertList(value)) + return `Value.list(${list})` + } + case "pointer": { + return `/* TODO deal with point removed point "${value.name}" */null as any` + } + } + throw Error(`Unknown type "${value.type}"`) + } + + function convertList(value: any) { + switch (value.subtype) { + case "string": { + return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify( + { + name: value.name || null, + minLength: null, + maxLength: null, + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2, + )}, ${JSON.stringify({ + masked: value?.spec?.masked || false, + placeholder: value?.spec?.placeholder || null, + patterns: value?.spec?.pattern + ? [ + { + regex: value.spec.pattern, + description: value?.spec?.["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + })})` + } + // case "number": { + // return `${rangeToTodoComment(value?.range)}List.number(${JSON.stringify( + // { + // name: value.name || null, + // minLength: null, + // maxLength: null, + // default: value.default || null, + // description: value.description || null, + // warning: value.warning || null, + // }, + // null, + // 2, + // )}, ${JSON.stringify({ + // integer: value?.spec?.integral || false, + // min: null, + // max: null, + // units: value?.spec?.units || null, + // placeholder: value?.spec?.placeholder || null, + // })})` + // } + case "enum": { + return "/* error!! list.enum */" + } + case "object": { + const specName = maybeNewConst( + value.name + "_spec", + convertInputSpec(value.spec.spec), + ) + return `${rangeToTodoComment(value?.range)}List.obj({ + name: ${JSON.stringify(value.name || null)}, + minLength: ${JSON.stringify(null)}, + maxLength: ${JSON.stringify(null)}, + default: ${JSON.stringify(value.default || null)}, + description: ${JSON.stringify(value.description || null)}, + }, { + spec: ${specName}, + displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, + uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, + })` + } + case "union": { + const variants = maybeNewConst( + value.name + "_variants", + convertVariants( + value.spec.variants, + value.spec["variant-names"] || {}, + ), + ) + const unionValueName = maybeNewConst( + value.name + "_union", + `${rangeToTodoComment(value?.range)} + Value.union({ + name: ${JSON.stringify(value?.spec?.tag?.name || null)}, + description: ${JSON.stringify( + value?.spec?.tag?.description || null, + )}, + warning: ${JSON.stringify(value?.spec?.tag?.warning || null)}, + default: ${JSON.stringify(value?.spec?.default || null)}, + }, ${variants}) + `, + ) + const listInputSpec = maybeNewConst( + value.name + "_list_inputSpec", + ` + InputSpec.of({ + "union": ${unionValueName} + }) + `, + ) + return `${rangeToTodoComment(value?.range)}List.obj({ + name:${JSON.stringify(value.name || null)}, + minLength:${JSON.stringify(null)}, + maxLength:${JSON.stringify(null)}, + default: [], + description: ${JSON.stringify(value.description || null)}, + warning: ${JSON.stringify(value.warning || null)}, + }, { + spec: ${listInputSpec}, + displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, + uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, + })` + } + } + throw new Error(`Unknown subtype "${value.subtype}"`) + } + + function convertVariants( + variants: Record, + variantNames: Record, + ): string { + let answer = "Variants.of({" + for (const [key, value] of Object.entries(variants)) { + const variantSpec = maybeNewConst(key, convertInputSpec(value)) + answer += `"${key}": {name: "${ + variantNames[key] || key + }", spec: ${variantSpec}},` + } + return `${answer}})` + } + + function getNextConstName(name: string, i = 0): string { + const newName = !i ? name : name + i + if (namedConsts.has(newName)) { + return getNextConstName(name, i + 1) + } + namedConsts.add(newName) + return newName + } +} + +function rangeToTodoComment(range: string | undefined) { + if (!range) return "" + return `/* TODO: Convert range for this value (${range})*/` +} + +// oldSpecToBuilder( +// "./inputSpec.ts", +// // Put inputSpec here +// {}, +// ) diff --git a/sdk/package/tsconfig.json b/sdk/package/tsconfig.json new file mode 100644 index 000000000..7b4d4f7d8 --- /dev/null +++ b/sdk/package/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "strict": true, + "preserveConstEnums": true, + "sourceMap": true, + "pretty": true, + "declaration": true, + "noImplicitAny": true, + "esModuleInterop": true, + "types": ["node", "jest"], + "moduleResolution": "node", + "skipLibCheck": true, + "module": "commonjs", + "outDir": "../dist", + "target": "es2018" + }, + "include": ["lib/**/*", "../base/lib/util/Hostname.ts"], + "exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"] +} diff --git a/system-images/compat/Cargo.lock b/system-images/compat/Cargo.lock index c1e0959fb..0f82176f4 100644 --- a/system-images/compat/Cargo.lock +++ b/system-images/compat/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -32,25 +32,26 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.12", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", - "getrandom 0.2.8", + "getrandom 0.2.12", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -77,6 +78,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -101,26 +108,74 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" dependencies = [ "backtrace", ] [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "ascii-canvas" @@ -133,9 +188,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener", @@ -144,9 +199,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" dependencies = [ "brotli", "flate2", @@ -175,18 +230,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -198,6 +253,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix 0.27.1", + "rand 0.8.5", +] + [[package]] name = "atty" version = "0.2.14" @@ -215,11 +280,88 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.7", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad46c3ec4e12f4a4b6835e173ba21c25e484c9d02b49770bf006ce5367c036" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -250,9 +392,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -262,9 +404,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-cookies" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" dependencies = [ "lalrpop", "lalrpop-util", @@ -313,9 +455,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" dependencies = [ "serde", ] @@ -340,13 +482,26 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq 0.1.5", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", ] [[package]] @@ -361,9 +516,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -376,9 +531,9 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "brotli" -version = "3.3.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -387,43 +542,31 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata 0.1.10", - "serde", -] - [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" @@ -442,9 +585,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -452,23 +595,23 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] name = "chumsky" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23170228b96236b5a7299057ac284a321457700bc8c41a4476052f0f4ba5349d" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.14.3", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -477,18 +620,18 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.3.1", ] [[package]] @@ -533,13 +676,47 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", - "clap_lex", - "indexmap 1.9.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", "strsim 0.10.0", "termcolor", "textwrap 0.16.0", ] +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.6.0", + "strsim 0.10.0", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -550,14 +727,10 @@ dependencies = [ ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "clap_lex" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "color-eyre" @@ -576,9 +749,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -586,6 +759,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "compat" version = "0.1.0" @@ -597,7 +776,7 @@ dependencies = [ "emver", "failure", "imbl-value", - "indexmap 1.9.2", + "indexmap 1.9.3", "itertools 0.10.5", "lazy_static", "linear-map", @@ -607,7 +786,7 @@ dependencies = [ "pest_derive", "rand 0.8.5", "regex", - "rust-argon2 1.0.0", + "rust-argon2 1.0.1", "serde", "serde_json", "serde_yaml 0.8.26", @@ -616,86 +795,58 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" -[[package]] -name = "container-init" -version = "0.1.0" -dependencies = [ - "async-stream", - "color-eyre", - "futures", - "helpers", - "imbl", - "nix 0.27.1", - "procfs", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tracing", - "tracing-error", - "tracing-futures", - "tracing-subscriber", - "yajrc 0.1.0 (git+https://github.com/dr-bonez/yajrc.git?branch=develop)", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -745,15 +896,16 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4b6aa369f41f5faa04bb80c9b1f4216ea81646ed6124d76ba5c49a7aafd9cd" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" dependencies = [ "cookie 0.16.2", "idna 0.2.3", "log", "publicsuffix", "serde", + "serde_derive", "serde_json", "time", "url", @@ -778,9 +930,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -788,33 +940,33 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" @@ -827,21 +979,43 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "cfg-if", + "bitflags 2.4.2", + "crossterm_winapi", + "futures-core", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", ] [[package]] @@ -852,9 +1026,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -874,9 +1048,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array", "subtle", @@ -884,22 +1058,21 @@ dependencies = [ [[package]] name = "csv" -version = "1.1.6" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" dependencies = [ - "bstr", "csv-core", - "itoa 0.4.8", + "itoa", "ryu", "serde", ] [[package]] name = "csv-core" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" dependencies = [ "memchr", ] @@ -951,57 +1124,13 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "cxx" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.107", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.86" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", ] [[package]] @@ -1025,7 +1154,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -1036,17 +1165,17 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.12.3", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -1054,9 +1183,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "der" @@ -1069,6 +1198,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1079,7 +1218,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -1103,7 +1242,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -1138,9 +1277,9 @@ checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" [[package]] name = "dotenvy" -version = "0.15.6" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "drain" @@ -1153,21 +1292,21 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature 2.0.0", + "signature 2.2.0", "spki", ] @@ -1188,7 +1327,7 @@ checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "serde", - "signature 2.0.0", + "signature 2.2.0", ] [[package]] @@ -1207,33 +1346,34 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ "curve25519-dalek 4.1.1", "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", + "subtle", "zeroize", ] [[package]] name = "either" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" dependencies = [ "serde", ] [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -1262,9 +1402,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" dependencies = [ "log", ] @@ -1283,9 +1423,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -1299,20 +1439,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", -] - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", + "syn 2.0.48", ] [[package]] @@ -1323,23 +1450,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1361,9 +1477,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", @@ -1387,18 +1503,15 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", "synstructure", ] [[package]] name = "fastrand" -version = "1.8.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fd-lock-rs" @@ -1421,22 +1534,28 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "filetime" -version = "0.2.19" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.42.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1445,9 +1564,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1487,9 +1606,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1511,9 +1630,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1526,9 +1645,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1536,15 +1655,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1564,38 +1683,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1633,9 +1752,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1644,9 +1763,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gpt" @@ -1654,7 +1773,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "crc", "log", "uuid", @@ -1673,17 +1792,36 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 1.9.2", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1696,14 +1834,21 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] [[package]] name = "hashbrown" @@ -1711,22 +1856,26 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.7", + "allocator-api2", +] [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.14.3", ] [[package]] @@ -1747,12 +1896,12 @@ dependencies = [ "lazy_async_pool", "models", "pin-project", + "rpc-toolkit 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "tokio", "tokio-stream", "tracing", - "yajrc 0.1.0 (git+https://github.com/dr-bonez/yajrc.git?branch=develop)", ] [[package]] @@ -1766,18 +1915,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -1793,9 +1933,9 @@ checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac 0.12.1", ] @@ -1821,32 +1961,66 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", ] [[package]] name = "http" -version = "0.2.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" dependencies = [ "bytes", "fnv", - "itoa 1.0.5", + "itoa", ] [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1858,40 +2032,53 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", "httparse", "httpdate", - "itoa 1.0.5", + "itoa", "pin-project-lite", - "socket2 0.4.7", + "socket2", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1899,51 +2086,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] [[package]] -name = "hyper-ws-listener" -version = "0.3.0" +name = "hyper-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbfe4981e45b0a7403a55d4af12f8d30e173e722409658c3857243990e72180" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" dependencies = [ - "anyhow", - "base64 0.21.4", - "env_logger", - "futures", - "hyper", - "log", - "sha-1", + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2", "tokio", - "tokio-tungstenite", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1983,11 +2170,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "imbl" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b4555023847ca2cd6fd11f20b553886e6981c7e8aee9b3e7e960b4b17fb440" +checksum = "978d142c8028edf52095703af2fad11d6f611af1246685725d6b850634647085" dependencies = [ "bitmaps", "imbl-sized-chunks", @@ -1999,9 +2196,9 @@ dependencies = [ [[package]] name = "imbl-sized-chunks" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6957ea0b2541c5ca561d3ef4538044af79f8a05a1eb3a3b148936aaceaa1076" +checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63" dependencies = [ "bitmaps", ] @@ -2009,7 +2206,7 @@ dependencies = [ [[package]] name = "imbl-value" version = "0.1.0" -source = "git+https://github.com/Start9Labs/imbl-value.git#929395141c3a882ac366c12ac9402d0ebaa2201b" +source = "git+https://github.com/Start9Labs/imbl-value.git#48dc39a762a3b4f9300d3b9f850cbd394e777ae0" dependencies = [ "imbl", "serde", @@ -2045,9 +2242,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -2056,12 +2253,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] @@ -2098,20 +2295,20 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.4" +name = "integer-encoding" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +checksum = "924df4f0e24e2e7f9cdd90babb0b96f93b20f3ecfa949ea9e6613756b8c8e1bf" dependencies = [ - "libc", - "windows-sys 0.42.0", + "async-trait", + "tokio", ] [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" dependencies = [ "serde", ] @@ -2128,14 +2325,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", + "hermit-abi 0.3.4", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -2177,24 +2373,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jaq-core" @@ -2202,10 +2392,10 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb52eeac20f256459e909bd4a03bb8c4fab6a1fdbb8ed52d00f644152df48ece" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", "dyn-clone", "hifijson", - "indexmap 1.9.2", + "indexmap 1.9.3", "itertools 0.10.5", "jaq-parse", "log", @@ -2236,12 +2426,12 @@ dependencies = [ [[package]] name = "josekit" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5754487a088f527b1407df470db8e654e4064dccbbe1fe850e0773721e9962b7" +checksum = "cd20997283339a19226445db97d632c8dc7adb6b8172537fe0e9e540fb141df2" dependencies = [ "anyhow", - "base64 0.21.4", + "base64 0.21.7", "flate2", "once_cell", "openssl", @@ -2254,9 +2444,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -2294,30 +2484,30 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "lalrpop" -version = "0.19.8" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30455341b0e18f276fa64540aff54deafb54c589de6aca68659c63dd2d5d823" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" dependencies = [ "ascii-canvas", - "atty", "bit-set", "diff", "ena", + "is-terminal", "itertools 0.10.5", "lalrpop-util", "petgraph", "pico-args", "regex", - "regex-syntax 0.6.28", + "regex-syntax 0.7.5", "string_cache", "term", "tiny-keccak", @@ -2326,9 +2516,9 @@ dependencies = [ [[package]] name = "lalrpop-util" -version = "0.19.8" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf796c978e9b4d983414f4caedc9273aa33ee214c5b887bd55fde84c85d2dc4" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" dependencies = [ "regex", ] @@ -2343,6 +2533,12 @@ dependencies = [ "futures", ] +[[package]] +name = "lazy_format" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e479e99b287d578ed5f6cd4c92cdf48db219088adb9c5b14f7c155b71dfba792" + [[package]] name = "lazy_static" version = "1.4.0" @@ -2354,21 +2550,32 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.149" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall 0.4.1", +] [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -2385,15 +2592,6 @@ dependencies = [ "serde_test", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2402,15 +2600,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -2433,9 +2631,15 @@ dependencies = [ [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "mbrman" @@ -2452,18 +2656,19 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if", "digest 0.10.7", ] [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -2485,9 +2690,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -2497,20 +2702,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2519,19 +2725,20 @@ dependencies = [ name = "models" version = "0.1.0" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "color-eyre", - "ed25519-dalek 2.0.0", + "ed25519-dalek 2.1.0", "emver", "ipnet", "lazy_static", "mbrman", + "num_enum", "openssl", "patch-db", "rand 0.8.5", "regex", "reqwest", - "rpc-toolkit", + "rpc-toolkit 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "sqlx", @@ -2622,7 +2829,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if", "libc", ] @@ -2663,9 +2870,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -2674,9 +2881,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", @@ -2733,9 +2940,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -2743,33 +2950,33 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.4", "libc", ] [[package]] name = "num_enum" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -2780,18 +2987,18 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.30.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2805,7 +3012,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "byteorder", "md-5", "sha2 0.10.8", @@ -2814,11 +3021,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -2829,13 +3036,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", ] [[package]] @@ -2846,18 +3053,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.5+3.1.3" +version = "300.2.1+3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -2868,9 +3075,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "overload" @@ -2908,6 +3115,20 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.8", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2920,22 +3141,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.6" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.4.1", "smallvec", - "windows-sys 0.42.0", + "windows-targets 0.48.5", ] [[package]] name = "paste" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "patch-db" @@ -2965,7 +3186,7 @@ version = "0.1.0" dependencies = [ "patch-db-macro-internals", "proc-macro2", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -2975,7 +3196,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -2999,25 +3220,26 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.5.3" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.5.3" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" dependencies = [ "pest", "pest_generator", @@ -3025,22 +3247,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.3" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", ] [[package]] name = "pest_meta" -version = "2.5.3" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" dependencies = [ "once_cell", "pest", @@ -3049,12 +3271,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.2", + "indexmap 2.1.0", ] [[package]] @@ -3068,28 +3290,28 @@ dependencies = [ [[package]] name = "pico-args" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3127,21 +3349,27 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" [[package]] name = "platforms" -version = "3.1.2" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "portable-atomic" -version = "1.4.3" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -3171,63 +3399,46 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.13.2" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "once_cell", - "thiserror", - "toml 0.5.10", + "toml_edit 0.21.0", ] [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "chrono", - "flate2", - "hex", - "lazy_static", - "rustix", -] - [[package]] name = "proptest" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.1", + "bitflags 2.4.2", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", "rusty-fork", "tempfile", "unarray", @@ -3241,7 +3452,7 @@ checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -3268,9 +3479,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -3340,7 +3551,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.12", ] [[package]] @@ -3385,26 +3596,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.8", - "redox_syscall 0.2.16", + "getrandom 0.2.12", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -3414,14 +3634,14 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax 0.6.28", + "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -3430,9 +3650,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" @@ -3446,32 +3666,23 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "bytes", "cookie 0.16.2", - "cookie_store 0.16.1", + "cookie_store 0.16.2", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -3522,50 +3733,78 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", + "getrandom 0.2.12", "libc", - "once_cell", - "spin 0.5.2", + "spin 0.9.8", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "rpassword" -version = "7.2.0" +version = "7.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" dependencies = [ "libc", "rtoolbox", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "rpc-toolkit" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5353673ffd8265292281141560d2b851e4da49e83e2f5e255fd473736d45ee10" +checksum = "c48252a30abb9426a3239fa8dfd2c8dd2647bb24db0b6145db2df04ae53fe647" dependencies = [ "clap 3.2.25", "futures", - "hyper", + "hyper 0.14.28", "lazy_static", "openssl", "reqwest", - "rpc-toolkit-macro", + "rpc-toolkit-macro 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_cbor 0.11.2", "serde_json", "thiserror", "tokio", "url", - "yajrc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "yajrc", +] + +[[package]] +name = "rpc-toolkit" +version = "0.2.3" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#9e989e23adb440bc72faa585b28e5aa2667a0a0d" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "clap 4.4.18", + "futures", + "http 1.0.0", + "http-body-util", + "imbl-value", + "itertools 0.12.0", + "lazy_format", + "lazy_static", + "openssl", + "pin-project", + "reqwest", + "rpc-toolkit-macro 0.2.2 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "url", + "yajrc", ] [[package]] @@ -3575,8 +3814,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e4b9cb00baf2d61bcd35e98d67dcb760382a3b4540df7e63b38d053c8a7b8b" dependencies = [ "proc-macro2", - "rpc-toolkit-macro-internals", - "syn 1.0.107", + "rpc-toolkit-macro-internals 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.109", +] + +[[package]] +name = "rpc-toolkit-macro" +version = "0.2.2" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#9e989e23adb440bc72faa585b28e5aa2667a0a0d" +dependencies = [ + "proc-macro2", + "rpc-toolkit-macro-internals 0.2.2 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", + "syn 1.0.109", ] [[package]] @@ -3587,27 +3836,36 @@ checksum = "d3e2ce21b936feaecdab9c9a8e75b9dca64374ccc11951a58045ad6559b75f42" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", +] + +[[package]] +name = "rpc-toolkit-macro-internals" +version = "0.2.2" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#9e989e23adb440bc72faa585b28e5aa2667a0a0d" +dependencies = [ + "itertools 0.12.0", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ - "byteorder", "const-oid", "digest 0.10.7", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "spki", "subtle", "zeroize", @@ -3615,42 +3873,42 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.1" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "rust-argon2" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" +checksum = "a5885493fdf0be6cdff808d1533ce878d21cfa49c7086fa00c66355cd9141bfc" dependencies = [ - "base64 0.13.1", + "base64 0.21.7", "blake2b_simd", - "constant_time_eq 0.1.5", + "constant_time_eq", "crossbeam-utils", ] [[package]] name = "rust-argon2" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e71971821b3ae0e769e4a4328dbcb517607b434db7697e9aba17203ec14e46a" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "blake2b_simd", - "constant_time_eq 0.3.0", + "constant_time_eq", ] [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" @@ -3663,99 +3921,138 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.6" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ - "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.1", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "base64 0.21.4", + "ring", + "untrusted", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustyline-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca4447465ceb8c01c253cc81660b242547c58e4a59c85b13294a6e70de8b9e" dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", + "crossterm", + "futures-channel", + "futures-util", + "pin-project", + "thingbuf", + "thiserror", + "unicode-segmentation", + "unicode-width", ] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -3777,9 +4074,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3790,9 +4087,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -3800,18 +4097,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.152" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] @@ -3829,7 +4126,7 @@ dependencies = [ name = "serde_cbor" version = "0.11.1" dependencies = [ - "half", + "half 1.8.2", "serde", ] @@ -3839,47 +4136,57 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ - "half", + "half 1.8.2", "serde", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ - "indexmap 1.9.2", - "itoa 1.0.5", + "indexmap 2.1.0", + "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] [[package]] name = "serde_test" -version = "1.0.152" +version = "1.0.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3611210d2d67e3513204742004d6ac6f589e521861dabb0f649b070eea8bed9e" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" dependencies = [ "serde", ] @@ -3891,22 +4198,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.5", + "itoa", "ryu", "serde", ] [[package]] name = "serde_with" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "chrono", "hex", - "indexmap 1.9.2", - "indexmap 2.0.2", + "indexmap 1.9.3", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", @@ -3915,14 +4222,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -3931,7 +4238,7 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap 1.9.2", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", @@ -3939,33 +4246,22 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" dependencies = [ - "indexmap 2.0.2", - "itoa 1.0.5", + "indexmap 2.1.0", + "itoa", "ryu", "serde", "unsafe-libyaml", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -4010,18 +4306,45 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -4034,9 +4357,9 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -4055,40 +4378,30 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.7" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" -dependencies = [ - "libc", - "winapi", -] +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -4111,9 +4424,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -4121,20 +4434,20 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.10.5", + "itertools 0.12.0", "nom", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4145,11 +4458,11 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", "atoi", "byteorder", "bytes", @@ -4166,13 +4479,13 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.2", + "indexmap 2.1.0", "log", "memchr", "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.21.10", "rustls-pemfile", "serde", "serde_json", @@ -4189,23 +4502,24 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ + "atomic-write-file", "dotenvy", "either", "heck", @@ -4220,7 +4534,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.107", + "syn 1.0.109", "tempfile", "tokio", "url", @@ -4228,13 +4542,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64 0.21.4", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.4.2", "byteorder", "bytes", "chrono", @@ -4250,7 +4564,7 @@ dependencies = [ "hex", "hkdf", "hmac 0.12.1", - "itoa 1.0.5", + "itoa", "log", "md-5", "memchr", @@ -4271,13 +4585,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64 0.21.4", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.4.2", "byteorder", "chrono", "crc", @@ -4291,7 +4605,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "home", - "itoa 1.0.5", + "itoa", "log", "md-5", "memchr", @@ -4311,9 +4625,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", "chrono", @@ -4330,6 +4644,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "urlencoding", ] [[package]] @@ -4353,9 +4668,9 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "regex-syntax 0.6.28", + "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.37", + "syn 2.0.48", "unicode-width", ] @@ -4382,18 +4697,19 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2180b3bc4955efd5661a97658d3cf4c8107e0d132f619195afe9486c13cca313" +checksum = "01f8f4ea73476c0aa5d5e6a75ce1e8634e2c3f82005ef3bbed21547ac57f2bf7" dependencies = [ - "ed25519-dalek 2.0.0", + "ed25519-dalek 2.1.0", "p256", "p384", + "p521", "rand_core 0.6.4", "rsa", "sec1", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "ssh-cipher", "ssh-encoding", "subtle", @@ -4408,17 +4724,19 @@ dependencies = [ "async-compression", "async-stream", "async-trait", + "axum", + "axum-server", "base32", - "base64 0.21.4", + "base64 0.21.7", "base64ct", "basic-cookies", + "blake3", "bytes", "chrono", "ciborium", - "clap 3.2.25", + "clap 4.4.18", "color-eyre", "console", - "container-init", "cookie 0.18.0", "cookie_store 0.20.0", "current_platform", @@ -4426,7 +4744,7 @@ dependencies = [ "divrem", "ed25519 2.2.3", "ed25519-dalek 1.0.1", - "ed25519-dalek 2.0.0", + "ed25519-dalek 2.1.0", "emver", "fd-lock-rs", "futures", @@ -4434,22 +4752,23 @@ dependencies = [ "helpers", "hex", "hmac 0.12.1", - "http", - "hyper", - "hyper-ws-listener", + "http 1.0.0", "imbl", "imbl-value", "include_dir", - "indexmap 2.0.2", + "indexmap 2.1.0", "indicatif", + "integer-encoding", "ipnet", "iprange", "isocountry", - "itertools 0.11.0", + "itertools 0.12.0", "jaq-core", "jaq-std", "josekit", "jsonpath_lib", + "lazy_async_pool", + "lazy_format", "lazy_static", "libc", "log", @@ -4460,6 +4779,7 @@ dependencies = [ "nom", "num", "num_enum", + "once_cell", "openssh-keys", "openssl", "p256", @@ -4475,15 +4795,16 @@ dependencies = [ "reqwest", "reqwest_cookie_store", "rpassword", - "rpc-toolkit", - "rust-argon2 2.0.0", - "scopeguard", + "rpc-toolkit 0.2.3 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", + "rust-argon2 2.1.0", + "rustyline-async", "semver", "serde", "serde_json", "serde_with", - "serde_yaml 0.9.25", + "serde_yaml 0.9.30", "sha2 0.10.8", + "shell-words", "simple-logging", "sqlx", "sscanf", @@ -4498,7 +4819,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.2", + "toml 0.8.8", "torut", "tracing", "tracing-error", @@ -4528,9 +4849,9 @@ dependencies = [ [[package]] name = "string_cache" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", @@ -4541,10 +4862,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -4563,15 +4885,15 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -4580,15 +4902,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.12.6" @@ -4597,7 +4925,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", "unicode-xid", ] @@ -4636,21 +4964,20 @@ checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", - "xattr 1.0.1", + "xattr 1.3.1", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall 0.2.16", - "remove_dir_all", - "winapi", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.52.0", ] [[package]] @@ -4688,24 +5015,34 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +[[package]] +name = "thingbuf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4706f1bfb859af03f099ada2de3cea3e515843c2d3e93b7893f16d94a37f9415" +dependencies = [ + "parking_lot", + "pin-project", +] + [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -4721,20 +5058,23 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ - "itoa 1.0.5", + "deranged", + "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -4742,15 +5082,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -4775,15 +5115,15 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -4793,20 +5133,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -4821,11 +5161,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls", + "rustls 0.22.2", + "rustls-pki-types", "tokio", ] @@ -4869,9 +5210,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", @@ -4883,9 +5224,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -4895,15 +5236,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.7.8" @@ -4918,21 +5250,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.21.0", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -4943,7 +5275,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -4952,11 +5284,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -4983,6 +5315,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -4991,9 +5345,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.39" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -5009,7 +5363,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] @@ -5055,20 +5409,20 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -5093,9 +5447,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559ac980345f7f5020883dd3bcacf176355225e01916f8c2efecad7534f682c6" +checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" dependencies = [ "async-trait", "cfg-if", @@ -5118,9 +5472,9 @@ dependencies = [ [[package]] name = "trust-dns-server" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4307166910ddf09378e651e9d4730c44900e9e0e1f157a6b955e48b539cd1d6" +checksum = "c540f73c2b2ec2f6c54eabd0900e7aafb747a820224b742f556e8faabb461bc7" dependencies = [ "async-trait", "bytes", @@ -5140,20 +5494,20 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.0.0", "httparse", "log", "native-tls", @@ -5166,35 +5520,35 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c6a006a6d3d6a6f143fda41cf4d1ad35110080687628c9f2117bd3cc7924f3" +checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa054ee5e2346187d631d2f1d1fd3b33676772d6d03a2d84e1c5213b31674ee" +checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.48", ] [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unarray" @@ -5204,24 +5558,24 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -5234,15 +5588,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -5258,24 +5612,24 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -5292,13 +5646,19 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.12", ] [[package]] @@ -5336,11 +5696,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -5358,9 +5717,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5368,24 +5727,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -5395,9 +5754,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5405,22 +5764,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" @@ -5437,9 +5796,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -5447,18 +5806,15 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "whoami" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" [[package]] name = "winapi" @@ -5478,9 +5834,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -5492,27 +5848,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.1", + "windows-targets 0.52.0", ] [[package]] @@ -5525,18 +5866,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.1" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows-targets 0.52.0", ] [[package]] @@ -5555,10 +5890,19 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.1" +name = "windows-targets" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] [[package]] name = "windows_aarch64_gnullvm" @@ -5567,10 +5911,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" +name = "windows_aarch64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" @@ -5579,10 +5923,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "windows_i686_gnu" -version = "0.42.1" +name = "windows_aarch64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" @@ -5591,10 +5935,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "windows_i686_msvc" -version = "0.42.1" +name = "windows_i686_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" @@ -5603,10 +5947,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" +name = "windows_i686_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" @@ -5615,10 +5959,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.1" +name = "windows_x86_64_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" @@ -5627,10 +5971,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" +name = "windows_x86_64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" @@ -5638,11 +5982,17 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] @@ -5677,29 +6027,20 @@ dependencies = [ [[package]] name = "xattr" -version = "1.0.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", + "linux-raw-sys", + "rustix", ] [[package]] name = "yajrc" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40687b4c165cb760e35730055c8840f36897e7c98099b2d3d66ba8cb624c79a" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "yajrc" -version = "0.1.0" -source = "git+https://github.com/dr-bonez/yajrc.git?branch=develop#72a22f7ac2197d7a5cdce4be601cf20e5280eec5" +checksum = "ce7af47ad983c2f8357333ef87d859e66deb7eef4bf6f9e1ae7b5e99044a48bf" dependencies = [ "anyhow", "serde", @@ -5722,29 +6063,48 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f355ab62ebe30b758c1f4ab096a306722c4b7dbfb9d8c07d18c70d71a945588" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", "hashbrown 0.13.2", "lazy_static", "serde", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", - "synstructure", + "syn 2.0.48", ] diff --git a/system-images/compat/Cargo.toml b/system-images/compat/Cargo.toml index e527c4150..d5fb24161 100644 --- a/system-images/compat/Cargo.toml +++ b/system-images/compat/Cargo.toml @@ -17,7 +17,7 @@ emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", ] } failure = "0.1.8" indexmap = { version = "1.6.2", features = ["serde"] } -imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" } +imbl-value = "0.1.2" itertools = "0.10.0" lazy_static = "1.4" linear-map = { version = "1.2", features = ["serde_impl"] } diff --git a/system-images/compat/Dockerfile b/system-images/compat/Dockerfile index d48ac3402..c01248b04 100644 --- a/system-images/compat/Dockerfile +++ b/system-images/compat/Dockerfile @@ -1,8 +1 @@ -FROM alpine:latest - -ARG ARCH - -RUN apk update && apk add duplicity curl -ADD ./target/$ARCH-unknown-linux-musl/release/compat /usr/local/bin/compat - -ENTRYPOINT ["compat"] +FROM start9/compat \ No newline at end of file diff --git a/system-images/compat/Makefile b/system-images/compat/Makefile index 86d881193..b6cd1bfec 100644 --- a/system-images/compat/Makefile +++ b/system-images/compat/Makefile @@ -11,10 +11,10 @@ clean: docker-images: mkdir docker-images -docker-images/aarch64.tar: Dockerfile target/aarch64-unknown-linux-musl/release/compat docker-images +docker-images/aarch64.tar: Dockerfile docker-images docker buildx build --build-arg ARCH=aarch64 --tag start9/x_system/compat --platform=linux/arm64 -o type=docker,dest=docker-images/aarch64.tar . -docker-images/x86_64.tar: Dockerfile target/x86_64-unknown-linux-musl/release/compat docker-images +docker-images/x86_64.tar: Dockerfile docker-images docker buildx build --build-arg ARCH=x86_64 --tag start9/x_system/compat --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . target/aarch64-unknown-linux-musl/release/compat: $(COMPAT_SRC) ../../core/Cargo.lock diff --git a/system-images/compat/src/config/mod.rs b/system-images/compat/src/config/mod.rs index ce591b06a..c677dd3d5 100644 --- a/system-images/compat/src/config/mod.rs +++ b/system-images/compat/src/config/mod.rs @@ -55,7 +55,7 @@ pub fn validate_configuration( Ok(_) => { // create temp config file serde_yaml::to_writer( - std::fs::File::create(config_path.with_extension("tmp"))?, + std::fs::create_file(config_path.with_extension("tmp"))?, &config, )?; std::fs::rename(config_path.with_extension("tmp"), config_path)?; diff --git a/web/README.md b/web/README.md index 213113775..feddf713e 100644 --- a/web/README.md +++ b/web/README.md @@ -3,33 +3,37 @@ StartOS web UIs are written in [Angular/Typescript](https://angular.io/docs) and leverage the [Ionic Framework](https://ionicframework.com/) component library. StartOS conditionally serves one of four Web UIs, depending on the state of the system and user choice. + - **install-wizard** - UI for installing StartOS, served on localhost. - **setup-wizard** - UI for setting up StartOS, served on start.local. -- **diagnostic-ui** - UI to display any error during server initialization, served on start.local. - **ui** - primary UI for administering StartOS, served on various hosts unique to the instance. Additionally, there are two libraries for shared code: + - **marketplace** - library code shared between the StartOS UI and Start9's [brochure marketplace](https://github.com/Start9Labs/brochure-marketplace). - **shared** - library code shared between the various web UIs and marketplace lib. ## Environment Setup #### Install NodeJS and NPM + - [Install nodejs](https://nodejs.org/en/) - [Install npm](https://www.npmjs.com/get-npm) #### Check that your versions match the ones below + ```sh node --version -v18.15.0 +v20.17.0 npm --version -v8.0.0 +v10.8.2 ``` #### Install and enable the Prettier extension for your text editor #### Clone StartOS and load the PatchDB submodule if you have not already + ```sh git clone https://github.com/Start9Labs/start-os.git cd start-os @@ -37,13 +41,17 @@ git submodule update --init --recursive ``` #### Move to web directory and install dependencies + ```sh cd web npm i npm run build:deps ``` +> Note if you are on **Windows** you need to install `make` for these scripts to work. Easiest way to do so is to install [Chocolatey](https://chocolatey.org/install) and then run `choco install make`. + #### Copy `config-sample.json` to a new file `config.json`. + ```sh cp config-sample.json config.json ``` @@ -59,10 +67,10 @@ You can develop using mocks (recommended to start) or against a live server. Eit ### Using mocks #### Start the standard development server + ```sh npm run start:install-wiz npm run start:setup -npm run start:dui npm run start:ui ``` @@ -71,6 +79,7 @@ npm run start:ui #### In `config.json`, set "useMocks" to `false` #### Copy `proxy.conf-sample.json` to a new file `proxy.conf.json` + ```sh cp proxy.conf-sample.json proxy.conf.json ``` @@ -78,6 +87,7 @@ cp proxy.conf-sample.json proxy.conf.json #### Replace every instance of "\\" with the hostname of your remote server #### Start the proxy development server + ```sh npm run start:ui:proxy ``` diff --git a/web/angular.json b/web/angular.json index 3a91dd00b..e3153139b 100644 --- a/web/angular.json +++ b/web/angular.json @@ -74,7 +74,8 @@ "with": "projects/ui/src/environments/environment.prod.ts" } ], - "outputHashing": "all" + "outputHashing": "all", + "extractLicenses": false }, "development": { "buildOptimizer": false, @@ -302,6 +303,7 @@ } ], "styles": [ + "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", "projects/shared/styles/variables.scss", "projects/shared/styles/global.scss", "projects/shared/styles/shared.scss", @@ -393,136 +395,6 @@ } } }, - "diagnostic-ui": { - "projectType": "application", - "schematics": {}, - "root": "projects/diagnostic-ui", - "sourceRoot": "projects/diagnostic-ui/src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/raw/diagnostic-ui", - "index": "projects/diagnostic-ui/src/index.html", - "main": "projects/diagnostic-ui/src/main.ts", - "polyfills": "projects/diagnostic-ui/src/polyfills.ts", - "tsConfig": "projects/diagnostic-ui/tsconfig.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "projects/shared/assets", - "output": "assets" - }, - { - "glob": "**/*.svg", - "input": "node_modules/ionicons/dist/ionicons/svg", - "output": "./svg" - } - ], - "styles": [ - "projects/shared/styles/variables.scss", - "projects/shared/styles/global.scss", - "projects/shared/styles/shared.scss", - "projects/diagnostic-ui/src/styles.scss" - ], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "projects/diagnostic-ui/src/environments/environment.ts", - "with": "projects/diagnostic-ui/src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ] - }, - "ci": { - "progress": false - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - }, - "development": { - "browserTarget": "diagnostic-ui:build:development" - } - }, - "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "diagnostic-ui:build" - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": [ - "projects/diagnostic-ui/src/**/*.ts", - "projects/diagnostic-ui/src/**/*.html" - ] - } - }, - "ionic-cordova-build": { - "builder": "@ionic/angular-toolkit:cordova-build", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - } - } - }, - "ionic-cordova-serve": { - "builder": "@ionic/angular-toolkit:cordova-serve", - "options": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build", - "devServerTarget": "diagnostic-ui:serve" - }, - "configurations": { - "production": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build:production", - "devServerTarget": "diagnostic-ui:serve:production" - } - } - } - } - }, "marketplace": { "projectType": "library", "root": "projects/marketplace", diff --git a/web/ionic.config.json b/web/ionic.config.json index ee434f78a..c5810bc10 100644 --- a/web/ionic.config.json +++ b/web/ionic.config.json @@ -17,12 +17,6 @@ "integrations": {}, "type": "angular", "root": "projects/setup-wizard" - }, - "diagnostic-ui": { - "name": "diagnostic-ui", - "integrations": {}, - "type": "angular", - "root": "projects/diagnostic-ui" } }, "defaultProject": "ui" diff --git a/web/lint-staged.config.js b/web/lint-staged.config.js index 80ea7cf8b..731cc9d5e 100644 --- a/web/lint-staged.config.js +++ b/web/lint-staged.config.js @@ -4,7 +4,6 @@ module.exports = { 'projects/ui/**/*.ts': () => 'npm run check:ui', 'projects/shared/**/*.ts': () => 'npm run check:shared', 'projects/marketplace/**/*.ts': () => 'npm run check:marketplace', - 'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui', 'projects/install-wizard/**/*.ts': () => 'npm run check:install-wiz', 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup', } diff --git a/web/package-lock.json b/web/package-lock.json index 160cb31bb..2faf0f318 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,13 @@ { "name": "startos-ui", - "version": "0.3.5.1", + "version": "0.3.6-alpha.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.3.5.1", + "version": "0.3.6-alpha.13", + "license": "MIT", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", @@ -20,19 +21,27 @@ "@angular/service-worker": "^14.2.2", "@ionic/angular": "^6.1.15", "@materia-ui/ngx-monaco-editor": "^6.0.0", - "@ng-web-apis/common": "^2.0.0", - "@ng-web-apis/mutation-observer": "^2.0.0", - "@ng-web-apis/resize-observer": "^2.0.0", + "@ng-web-apis/common": "^3.2.3", + "@ng-web-apis/mutation-observer": "^3.2.3", + "@ng-web-apis/resize-observer": "^3.2.3", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.20.0", - "@taiga-ui/cdk": "3.20.0", - "@taiga-ui/core": "3.20.0", - "@taiga-ui/icons": "3.20.0", - "@taiga-ui/kit": "3.20.0", + "@start9labs/start-sdk": "file:../sdk/baseDist", + "@taiga-ui/addon-charts": "3.96.0", + "@taiga-ui/addon-commerce": "3.96.0", + "@taiga-ui/cdk": "3.96.0", + "@taiga-ui/core": "3.96.0", + "@taiga-ui/experimental": "3.96.0", + "@taiga-ui/icons": "3.96.0", + "@taiga-ui/kit": "3.96.0", + "@tinkoff/ng-dompurify": "4.0.0", + "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "cbor": "npm:@jprochazk/cbor@^0.4.9", "cbor-web": "^8.1.0", "core-js": "^3.21.1", @@ -43,15 +52,17 @@ "jose": "^4.9.0", "js-yaml": "^4.1.0", "marked": "^4.0.0", + "mime": "^4.0.3", "monaco-editor": "^0.33.0", "mustache": "^4.2.0", "ng-qrcode": "^7.0.0", "node-jose": "^2.2.0", - "patch-db-client": "file: ../../../patch-db/client", + "patch-db-client": "file:../patch-db/client", + "path-browserify": "^1.0.1", "pbkdf2": "^3.1.2", "rxjs": "^7.8.1", "swiper": "^8.2.4", - "ts-matches": "^5.2.1", + "ts-matches": "^6.1.0", "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" @@ -105,3845 +116,2512 @@ "rxjs": ">=7.0.0" } }, - "../patch-db/client/node_modules/@babel/code-frame": { - "version": "7.21.4", - "dev": true, + "../sdk/baseDist": { + "name": "@start9labs/start-sdk-base", "license": "MIT", "dependencies": { - "@babel/highlight": "^7.18.6" + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime-types": "^2.1.35", + "ts-matches": "^6.2.1", + "yaml": "^2.2.2" }, - "engines": { - "node": ">=6.9.0" + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/lodash.merge": "^4.6.2", + "@types/mime-types": "^2.1.4", + "jest": "^29.4.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", + "tsx": "^4.7.1", + "typescript": "^5.0.4" } }, - "../patch-db/client/node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", + "node_modules/@adobe/css-tools": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", + "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "../patch-db/client/node_modules/@babel/highlight": { - "version": "7.18.6", + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0.0" } }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, + "node_modules/@angular-devkit/architect": { + "version": "0.1402.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.13.tgz", + "integrity": "sha512-n0ISBuvkZHoOpAzuAZql1TU9VLHUE9e/a9g4VNOPHewjMzpN02VqeGKvJfOCKtzkCs6gVssIlILm2/SXxkIFxQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@angular-devkit/core": "14.2.13", + "rxjs": "6.6.7" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "tslib": "^1.9.0" }, "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" + "npm": ">=2.0.0" } }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true, + "license": "0BSD" }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", + "node_modules/@angular-devkit/build-angular": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.13.tgz", + "integrity": "sha512-FJZKQ3xYFvEJ807sxVy4bCVyGU2NMl3UUPNfLIdIdzwwDEP9tx/cc+c4VtVPEZZfU8jVenu8XOvL6L0vpjt3yg==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1402.13", + "@angular-devkit/build-webpack": "0.1402.13", + "@angular-devkit/core": "14.2.13", + "@babel/core": "7.18.10", + "@babel/generator": "7.18.12", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.18.10", + "@babel/plugin-transform-async-to-generator": "7.18.6", + "@babel/plugin-transform-runtime": "7.18.10", + "@babel/preset-env": "7.18.10", + "@babel/runtime": "7.18.9", + "@babel/template": "7.18.10", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "14.2.13", + "ansi-colors": "4.1.3", + "babel-loader": "8.2.5", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.9.1", + "cacache": "16.1.2", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.1", + "esbuild-wasm": "0.15.5", + "glob": "8.0.3", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.1.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.0.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "mini-css-extract-plugin": "2.6.1", + "minimatch": "5.1.0", + "open": "8.4.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "6.0.1", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-import": "15.0.0", + "postcss-loader": "7.0.1", + "postcss-preset-env": "7.8.0", + "regenerator-runtime": "0.13.9", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.54.4", + "sass-loader": "13.0.2", + "semver": "7.5.3", + "source-map-loader": "4.0.0", + "source-map-support": "0.5.21", + "stylus": "0.59.0", + "stylus-loader": "7.0.0", + "terser": "5.14.2", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.4.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "5.3.3", + "webpack-dev-server": "4.11.0", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.15.5" + }, + "peerDependencies": { + "@angular/compiler-cli": "^14.0.0", + "@angular/localize": "^14.0.0", + "@angular/service-worker": "^14.0.0", + "karma": "^6.3.0", + "ng-packagr": "^14.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.6.2 <4.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } } }, - "../patch-db/client/node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" } }, - "../patch-db/client/node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } + "license": "MIT" }, - "../patch-db/client/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", "dev": true, "license": "MIT" }, - "../patch-db/client/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" } }, - "../patch-db/client/node_modules/@tsconfig/node10": { - "version": "1.0.9", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@tsconfig/node12": { - "version": "1.0.11", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", "dev": true, "license": "MIT" }, - "../patch-db/client/node_modules/@tsconfig/node14": { - "version": "1.0.3", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } }, - "../patch-db/client/node_modules/@tsconfig/node16": { - "version": "1.0.4", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } }, - "../patch-db/client/node_modules/@types/node": { - "version": "18.15.0", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } }, - "../patch-db/client/node_modules/@types/parse-json": { - "version": "4.0.0", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", "dev": true, "license": "MIT" }, - "../patch-db/client/node_modules/@types/uuid": { - "version": "8.3.1", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } }, - "../patch-db/client/node_modules/acorn": { - "version": "8.8.2", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" } }, - "../patch-db/client/node_modules/acorn-walk": { - "version": "8.2.0", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.4.0" + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" } }, - "../patch-db/client/node_modules/aggregate-error": { - "version": "3.1.0", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", "dev": true, "license": "MIT", "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" } }, - "../patch-db/client/node_modules/ansi-escapes": { - "version": "4.3.2", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" } }, - "../patch-db/client/node_modules/ansi-regex": { - "version": "6.0.1", + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "../patch-db/client/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "../patch-db/client/node_modules/arg": { - "version": "4.1.3", + "node_modules/@angular-devkit/build-angular/node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true, "license": "MIT" }, - "../patch-db/client/node_modules/argparse": { - "version": "1.0.10", + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "MIT" }, - "../patch-db/client/node_modules/astral-regex": { - "version": "2.0.0", + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, "engines": { - "node": ">=8" + "npm": ">=2.0.0" } }, - "../patch-db/client/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/brace-expansion": { - "version": "1.1.11", + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "0BSD" }, - "../patch-db/client/node_modules/braces": { - "version": "3.0.2", + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/builtin-modules": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "../patch-db/client/node_modules/callsites": { - "version": "3.1.0", + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "0BSD" }, - "../patch-db/client/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@angular-devkit/build-angular/node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" }, "engines": { - "node": ">=10" + "node": ">=10.13.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, - "../patch-db/client/node_modules/ci-info": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/clean-stack": { - "version": "2.2.0", + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1402.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.13.tgz", + "integrity": "sha512-K27aJmuw86ZOdiu5PoGeGDJ2v7g2ZCK0bGwc8jzkjTLRfvd4FRKIIZumGv3hbQ3vQRLikiU6WMDRTFyCZky/EA==", "dev": true, "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1402.13", + "rxjs": "6.6.7" + }, "engines": { - "node": ">=6" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" } }, - "../patch-db/client/node_modules/cli-cursor": { - "version": "3.1.0", + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "restore-cursor": "^3.1.0" + "tslib": "^1.9.0" }, "engines": { - "node": ">=8" + "npm": ">=2.0.0" } }, - "../patch-db/client/node_modules/cli-truncate": { - "version": "3.1.0", + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/core": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.13.tgz", + "integrity": "sha512-aIefeZcbjghQg/V6U9CTLtyB5fXDJ63KwYqVYkWP+i0XriS5A9puFgq2u/OVsWxAfYvqpDqp5AdQ0g0bi3CAsA==", "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "../patch-db/client/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "tslib": "^1.9.0" }, "engines": { - "node": ">=7.0.0" + "npm": ">=2.0.0" } }, - "../patch-db/client/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, - "../patch-db/client/node_modules/commander": { - "version": "10.0.1", - "dev": true, + "node_modules/@angular-devkit/schematics": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.13.tgz", + "integrity": "sha512-2zczyeNzeBcrT2HOysv52X9SH3tZoHfWJvVf6H0SIa74rfDKEl7hFpKNXnh3x8sIMLj5mZn05n5RCqGxCczcIg==", "license": "MIT", + "dependencies": { + "@angular-devkit/core": "14.2.13", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, "engines": { - "node": ">=14" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "../patch-db/client/node_modules/compare-versions": { - "version": "3.6.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/cosmiconfig": { - "version": "7.1.0", - "dev": true, - "license": "MIT", + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "tslib": "^1.9.0" }, "engines": { - "node": ">=10" + "npm": ">=2.0.0" } }, - "../patch-db/client/node_modules/create-require": { - "version": "1.1.1", - "dev": true, - "license": "MIT" + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, - "../patch-db/client/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, + "node_modules/@angular/animations": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.3.0.tgz", + "integrity": "sha512-QoBcIKy1ZiU+4qJsAh5Ls20BupWiXiZzKb0s6L9/dntPt5Msr4Ao289XR2P6O1L+kTsCprH9Kt41zyGQ/bkRqg==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "tslib": "^2.3.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "14.3.0" } }, - "../patch-db/client/node_modules/debug": { - "version": "4.3.4", - "dev": true, + "node_modules/@angular/cli": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.13.tgz", + "integrity": "sha512-I5EepRem2CCyS3GDzQxZ2ZrqQwVqoGoLY+ZQhsK1QGWUnUyFOjbv3OlUGxRUYwcedu19V1EBAKjmQ96HzMIcVQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "@angular-devkit/architect": "0.1402.13", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", + "@schematics/angular": "14.2.13", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "debug": "4.3.4", + "ini": "3.0.0", + "inquirer": "8.2.4", + "jsonc-parser": "3.1.0", + "npm-package-arg": "9.1.0", + "npm-pick-manifest": "7.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "13.6.2", + "resolve": "1.22.1", + "semver": "7.5.3", + "symbol-observable": "4.0.0", + "uuid": "8.3.2", + "yargs": "17.5.1" }, - "engines": { - "node": ">=6.0" + "bin": { + "ng": "bin/ng.js" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../patch-db/client/node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "../patch-db/client/node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/error-ex": { - "version": "1.3.2", - "dev": true, + "node_modules/@angular/common": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.3.0.tgz", + "integrity": "sha512-pV9oyG3JhGWeQ+TFB0Qub6a1VZWMNZ6/7zEopvYivdqa5yDLLDSBRWb6P80RuONXyGnM1pa7l5nYopX+r/23GQ==", "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "../patch-db/client/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "../patch-db/client/node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "tslib": "^2.3.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "14.3.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "../patch-db/client/node_modules/execa": { - "version": "7.1.1", - "dev": true, + "node_modules/@angular/compiler": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.3.0.tgz", + "integrity": "sha512-E15Rh0t3vA+bctbKnBCaDmLvc3ix+ZBt6yFZmhZalReQ+KpOlvOJv+L9oiFEgg+rYVl2QdvN7US1fvT0PqswLw==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" + "tslib": "^2.3.0" }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": "^14.15.0 || >=16.10.0" }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "peerDependencies": { + "@angular/core": "14.3.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } } }, - "../patch-db/client/node_modules/fill-range": { - "version": "7.0.1", + "node_modules/@angular/compiler-cli": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.3.0.tgz", + "integrity": "sha512-eoKpKdQ2X6axMgzcPUMZVYl3bIlTMzMeTo5V29No4BzgiUB+QoOTYGNJZkGRyqTNpwD9uSBJvmT2vG9+eC4ghQ==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "@babel/core": "^7.17.2", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.26.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/compiler": "14.3.0", + "typescript": ">=4.6.2 <4.9" } }, - "../patch-db/client/node_modules/find-up": { - "version": "5.0.0", - "dev": true, + "node_modules/@angular/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.3.0.tgz", + "integrity": "sha512-wYiwItc0Uyn4FWZ/OAx/Ubp2/WrD3EgUJ476y1XI7yATGPF8n9Ld5iCXT08HOvc4eBcYlDfh90kTXR6/MfhzdQ==", "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "tslib": "^2.3.0" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || >=16.10.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0" } }, - "../patch-db/client/node_modules/find-versions": { - "version": "4.0.0", - "dev": true, + "node_modules/@angular/forms": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.3.0.tgz", + "integrity": "sha512-fBZZC2UFMom2AZPjGQzROPXFWO6kvCsPDKctjJwClVC8PuMrkm+RRyiYRdBbt2qxWHEqOZM2OCQo73xUyZOYHw==", "license": "MIT", "dependencies": { - "semver-regex": "^3.1.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || >=16.10.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@angular/common": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "../patch-db/client/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/get-stream": { - "version": "6.0.1", + "node_modules/@angular/language-service": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-14.3.0.tgz", + "integrity": "sha512-Sij3OQzj1UGs1O8H9PxVAY/o27+oqZwQRnib66rsWvtbIBTjHp4FV3dTs5iVcr62GGv4V4Mff/2I82NP10GPQg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || >=16.10.0" } }, - "../patch-db/client/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", + "node_modules/@angular/platform-browser": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.3.0.tgz", + "integrity": "sha512-w9Y3740UmTz44T0Egvc+4QV9sEbO61L+aRHbpkLTJdlEGzHByZvxJmJyBYmdqeyTPwc/Zpy7c02frlpfAlyB7A==", + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "tslib": "^2.3.0" }, "engines": { - "node": "*" + "node": "^14.15.0 || >=16.10.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@angular/animations": "14.3.0", + "@angular/common": "14.3.0", + "@angular/core": "14.3.0" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } } }, - "../patch-db/client/node_modules/has": { - "version": "1.0.3", - "dev": true, + "node_modules/@angular/platform-browser-dynamic": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.3.0.tgz", + "integrity": "sha512-rneZiMrIiYRhrkQvdL40E2ErKRn4Zdo6EtjBM9pAmWeyoM8oMnOZb9gz5vhrkNWg06kVMVg0yKqluP5How7j3A==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.1" + "tslib": "^2.3.0" }, "engines": { - "node": ">= 0.4.0" + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "14.3.0", + "@angular/compiler": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0" } }, - "../patch-db/client/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, + "node_modules/@angular/pwa": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular/pwa/-/pwa-14.2.13.tgz", + "integrity": "sha512-WPkTgT3+VC/KeZMydZnTQJWTG5IVTSdkJfqmNQWfHXzpmm1CG4KvFRj3xEOXvaDmcL56nnqKhL/o66kpai15Qw==", "license": "MIT", + "dependencies": { + "@angular-devkit/schematics": "14.2.13", + "@schematics/angular": "14.2.13", + "parse5-html-rewriting-stream": "6.0.1" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/cli": "^14.0.0 || ^14.0.0-next || ^14.1.0-next" + }, + "peerDependenciesMeta": { + "@angular/cli": { + "optional": true + } } }, - "../patch-db/client/node_modules/human-signals": { - "version": "4.3.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@angular/router": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.3.0.tgz", + "integrity": "sha512-uip0V7w7k7xyxxpTPbr7EuMnYLj3FzJrwkLVJSEw3TMMGHt5VU5t4BBa9veGZOta2C205XFrTAHnp8mD+XYY1w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, "engines": { - "node": ">=14.18.0" + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "../patch-db/client/node_modules/husky": { - "version": "4.3.8", - "dev": true, - "hasInstallScript": true, + "node_modules/@angular/service-worker": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-14.3.0.tgz", + "integrity": "sha512-i5O7m1gQijWm7cgva0XTmOVBFrPrttNxFDwoMLMYCh8rHOCQUQ4DcVO1qTBPWU4SrY5BYPEvR+r05dYQLFYCBw==", "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" + "tslib": "^2.3.0" }, "bin": { - "husky-run": "bin/run.js", - "husky-upgrade": "lib/upgrader/bin.js" + "ngsw-config": "ngsw-config.js" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || >=16.10.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/husky" + "peerDependencies": { + "@angular/common": "14.3.0", + "@angular/core": "14.3.0" } }, - "../patch-db/client/node_modules/import-fresh": { - "version": "3.3.0", + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/indent-string": { - "version": "4.0.0", + "node_modules/@babel/compat-data": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/is-core-module": { - "version": "2.12.1", + "node_modules/@babel/core": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "dev": true, "license": "MIT", "dependencies": { - "has": "^1.0.3" + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "../patch-db/client/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "../patch-db/client/node_modules/is-number": { - "version": "7.0.0", + "node_modules/@babel/generator": { + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.10", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, "engines": { - "node": ">=0.12.0" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/is-stream": { - "version": "3.0.0", + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.0.0" } }, - "../patch-db/client/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/js-yaml": { - "version": "3.14.1", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@babel/types": "^7.18.6" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/lilconfig": { - "version": "2.1.0", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/lines-and-columns": { - "version": "1.2.4", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "../patch-db/client/node_modules/lint-staged": { - "version": "13.2.2", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", - "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/lint-staged" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/lint-staged/node_modules/chalk": { - "version": "5.2.0", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "@babel/types": "^7.25.9" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.1", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", - "engines": { - "node": ">= 14" + "bin": { + "semver": "bin/semver.js" } }, - "../patch-db/client/node_modules/listr2": { - "version": "5.0.8", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/listr2/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/listr2/node_modules/cli-truncate": { - "version": "2.1.0", + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.4.0-0" } }, - "../patch-db/client/node_modules/listr2/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "../patch-db/client/node_modules/listr2/node_modules/slice-ansi": { - "version": "3.0.0", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "@babel/types": "^7.24.7" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/listr2/node_modules/string-width": { - "version": "4.2.3", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/listr2/node_modules/strip-ansi": { - "version": "6.0.1", + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/locate-path": { - "version": "6.0.0", + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/log-update": { - "version": "4.0.0", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/string-width": { - "version": "4.2.3", + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/strip-ansi": { - "version": "6.0.1", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/make-error": { - "version": "1.3.6", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "../patch-db/client/node_modules/merge-stream": { - "version": "2.0.0", + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "../patch-db/client/node_modules/micromatch": { - "version": "4.0.5", + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8.6" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/mimic-fn": { - "version": "4.0.0", + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/minimatch": { - "version": "3.1.2", + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/minimist": { - "version": "1.2.8", + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/mkdirp": { - "version": "0.5.6", + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "@babel/types": "^7.26.3" }, "bin": { - "mkdirp": "bin/cmd.js" + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "../patch-db/client/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/normalize-path": { - "version": "3.0.0", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/npm-run-path": { - "version": "5.1.0", + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^4.0.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "../patch-db/client/node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/object-inspect": { - "version": "1.12.3", + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/once": { - "version": "1.4.0", + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "wrappy": "1" + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "../patch-db/client/node_modules/onetime": { - "version": "6.0.0", + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/opencollective-postinstall": { - "version": "2.0.3", + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", "dev": true, "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/p-limit": { - "version": "3.1.0", + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/p-locate": { - "version": "5.0.0", + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/p-map": { - "version": "4.0.0", + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dev": true, "license": "MIT", "dependencies": { - "aggregate-error": "^3.0.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/parent-module": { - "version": "1.0.1", + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/parse-json": { - "version": "5.2.0", + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/path-exists": { - "version": "4.0.0", + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/path-is-absolute": { - "version": "1.0.1", + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/path-key": { - "version": "3.1.1", + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/picomatch": { - "version": "2.3.1", + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { - "node": ">=8.6" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/pidtree": { - "version": "0.6.0", + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", "dev": true, "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { - "node": ">=0.10" + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/pkg-dir": { - "version": "5.0.0", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^5.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/please-upgrade-node": { - "version": "3.2.0", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", "dependencies": { - "semver-compare": "^1.0.0" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/prettier": { - "version": "2.8.8", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/resolve": { - "version": "1.22.2", + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/resolve-from": { - "version": "4.0.0", + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/restore-cursor": { - "version": "3.1.0", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/rfdc": { - "version": "1.3.0", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/rxjs": { - "version": "7.8.1", - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/semver": { - "version": "5.7.1", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/semver-compare": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/semver-regex": { - "version": "3.1.4", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/shebang-command": { - "version": "2.0.0", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/shebang-regex": { - "version": "3.0.0", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/slash": { - "version": "3.0.0", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/slice-ansi": { - "version": "5.0.0", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/sorted-btree": { - "version": "1.5.0", - "license": "MIT" - }, - "../patch-db/client/node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "../patch-db/client/node_modules/string-argv": { - "version": "0.3.2", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + }, "engines": { - "node": ">=0.6.19" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/string-width": { - "version": "5.1.2", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/strip-ansi": { - "version": "7.1.0", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/strip-final-newline": { - "version": "3.0.0", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, "engines": { - "node": ">= 0.4" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/through": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/to-regex-range": { - "version": "5.0.1", + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=8.0" + "node": ">=6.9.0" } }, - "../patch-db/client/node_modules/ts-node": { - "version": "10.9.1", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", "dev": true, "license": "MIT", "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" + "@babel/helper-plugin-utils": "^7.25.9" }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslib": { - "version": "2.5.3", - "license": "0BSD" - }, - "../patch-db/client/node_modules/tslint": { - "version": "6.1.3", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=4.8.0" + "node": ">=6.9.0" }, "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/tslint/node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tslint/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../patch-db/client/node_modules/tsutils": { - "version": "2.29.0", + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../patch-db/client/node_modules/type-fest": { - "version": "0.21.3", + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/typescript": { - "version": "4.9.5", + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=4.2.0" - } - }, - "../patch-db/client/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/which": { - "version": "2.0.2", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/which-pm-runs": { - "version": "1.1.0", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/wrap-ansi": { - "version": "7.0.0", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/yaml": { - "version": "1.10.2", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">= 6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/yn": { - "version": "3.1.1", + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "../patch-db/client/node_modules/yocto-queue": { - "version": "0.1.0", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@adobe/css-tools": { - "version": "4.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/architect": { - "version": "0.1402.3", - "devOptional": true, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", + "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", + "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "14.2.3", - "rxjs": "6.6.7" + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", + "semver": "^6.3.0" }, "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "6.6.7", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "npm": ">=2.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/architect/node_modules/tslib": { - "version": "1.14.1", - "devOptional": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/build-angular": { - "version": "14.2.3", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/build-webpack": "0.1402.3", - "@angular-devkit/core": "14.2.3", - "@babel/core": "7.18.10", - "@babel/generator": "7.18.12", - "@babel/helper-annotate-as-pure": "7.18.6", - "@babel/plugin-proposal-async-generator-functions": "7.18.10", - "@babel/plugin-transform-async-to-generator": "7.18.6", - "@babel/plugin-transform-runtime": "7.18.10", - "@babel/preset-env": "7.18.10", - "@babel/runtime": "7.18.9", - "@babel/template": "7.18.10", - "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.2.3", - "ansi-colors": "4.1.3", - "babel-loader": "8.2.5", - "babel-plugin-istanbul": "6.1.1", - "browserslist": "^4.9.1", - "cacache": "16.1.2", - "copy-webpack-plugin": "11.0.0", - "critters": "0.0.16", - "css-loader": "6.7.1", - "esbuild-wasm": "0.15.5", - "glob": "8.0.3", - "https-proxy-agent": "5.0.1", - "inquirer": "8.2.4", - "jsonc-parser": "3.1.0", - "karma-source-map-support": "1.4.0", - "less": "4.1.3", - "less-loader": "11.0.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.0", - "mini-css-extract-plugin": "2.6.1", - "minimatch": "5.1.0", - "open": "8.4.0", - "ora": "5.4.1", - "parse5-html-rewriting-stream": "6.0.1", - "piscina": "3.2.0", - "postcss": "8.4.16", - "postcss-import": "15.0.0", - "postcss-loader": "7.0.1", - "postcss-preset-env": "7.8.0", - "regenerator-runtime": "0.13.9", - "resolve-url-loader": "5.0.0", - "rxjs": "6.6.7", - "sass": "1.54.4", - "sass-loader": "13.0.2", - "semver": "7.3.7", - "source-map-loader": "4.0.0", - "source-map-support": "0.5.21", - "stylus": "0.59.0", - "stylus-loader": "7.0.0", - "terser": "5.14.2", - "text-table": "0.2.0", - "tree-kill": "1.2.2", - "tslib": "2.4.0", - "webpack": "5.74.0", - "webpack-dev-middleware": "5.3.3", - "webpack-dev-server": "4.11.0", - "webpack-merge": "5.8.0", - "webpack-subresource-integrity": "5.1.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.15.5" + "node": ">=6.9.0" }, "peerDependencies": { - "@angular/compiler-cli": "^14.0.0", - "@angular/localize": "^14.0.0", - "@angular/service-worker": "^14.0.0", - "karma": "^6.3.0", - "ng-packagr": "^14.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=4.6.2 <4.9" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { - "version": "14.2.3", + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">=6.9.0" }, "peerDependencies": { - "@angular/compiler-cli": "^14.0.0", - "typescript": ">=4.6.2 <4.9", - "webpack": "^5.54.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { - "version": "6.6.7", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "npm": ">=2.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1402.3", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1402.3", - "rxjs": "6.6.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">=6.9.0" }, "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "6.6.7", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "npm": ">=2.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { - "version": "1.14.1", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", "dev": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/core": { - "version": "14.2.3", - "devOptional": true, "license": "MIT", "dependencies": { - "ajv": "8.11.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.1.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "6.6.7", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@angular-devkit/core/node_modules/tslib": { - "version": "1.14.1", - "devOptional": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/schematics": { - "version": "14.2.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "14.2.3", - "jsonc-parser": "3.1.0", - "magic-string": "0.26.2", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "6.6.7", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/tslib": { - "version": "1.14.1", - "devOptional": true, - "license": "0BSD" - }, - "node_modules/@angular/animations": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/core": "14.2.2" - } - }, - "node_modules/@angular/cli": { - "version": "14.2.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/core": "14.2.3", - "@angular-devkit/schematics": "14.2.3", - "@schematics/angular": "14.2.3", - "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "debug": "4.3.4", - "ini": "3.0.0", - "inquirer": "8.2.4", - "jsonc-parser": "3.1.0", - "npm-package-arg": "9.1.0", - "npm-pick-manifest": "7.0.1", - "open": "8.4.0", - "ora": "5.4.1", - "pacote": "13.6.2", - "resolve": "1.22.1", - "semver": "7.3.7", - "symbol-observable": "4.0.0", - "uuid": "8.3.2", - "yargs": "17.5.1" - }, - "bin": { - "ng": "bin/ng.js" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/common": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/core": "14.2.2", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/core": "14.2.2" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } - } - }, - "node_modules/@angular/compiler-cli": { - "version": "14.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.17.2", - "chokidar": "^3.0.0", - "convert-source-map": "^1.5.1", - "dependency-graph": "^0.11.0", - "magic-string": "^0.26.0", - "reflect-metadata": "^0.1.2", - "semver": "^7.0.0", - "sourcemap-codec": "^1.4.8", - "tslib": "^2.3.0", - "yargs": "^17.2.1" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/main-ngcc.js" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/compiler": "14.2.2", - "typescript": ">=4.6.2 <4.9" - } - }, - "node_modules/@angular/core": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.11.4" - } - }, - "node_modules/@angular/forms": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/language-service": { - "version": "14.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || >=16.10.0" - } - }, - "node_modules/@angular/platform-browser": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/animations": "14.2.2", - "@angular/common": "14.2.2", - "@angular/core": "14.2.2" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/compiler": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2" - } - }, - "node_modules/@angular/pwa": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "@angular-devkit/schematics": "14.1.0", - "@schematics/angular": "14.1.0", - "parse5-html-rewriting-stream": "6.0.1" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/cli": "^14.0.0 || ^14.0.0-next || ^14.1.0-next" - }, - "peerDependenciesMeta": { - "@angular/cli": { - "optional": true - } - } - }, - "node_modules/@angular/pwa/node_modules/@angular-devkit/core": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "ajv": "8.11.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.1.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/pwa/node_modules/@angular-devkit/schematics": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "14.1.0", - "jsonc-parser": "3.1.0", - "magic-string": "0.26.2", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/pwa/node_modules/@schematics/angular": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", - "jsonc-parser": "3.1.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/pwa/node_modules/rxjs": { - "version": "6.6.7", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@angular/pwa/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@angular/router": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/service-worker": { - "version": "14.2.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "bin": { - "ngsw-config": "ngsw-config.js" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2" - } - }, - "node_modules/@assemblyscript/loader": { - "version": "0.10.1", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.18.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-module-transforms": "^7.18.9", - "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.10", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.18.10", - "@babel/types": "^7.18.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.18.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.10", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.19.1", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.18.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.18.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.18.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.18.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.9", - "babel-plugin-polyfill-corejs2": "^0.3.2", - "babel-plugin-polyfill-corejs3": "^0.5.3", - "babel-plugin-polyfill-regenerator": "^0.4.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -3951,6 +2629,8 @@ }, "node_modules/@babel/preset-env": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, "license": "MIT", "dependencies": { @@ -4038,7 +2718,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -4046,7 +2728,9 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", "dev": true, "license": "MIT", "dependencies": { @@ -4057,11 +2741,13 @@ "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/runtime": { "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "dev": true, "license": "MIT", "dependencies": { @@ -4073,6 +2759,8 @@ }, "node_modules/@babel/template": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "license": "MIT", "dependencies": { @@ -4085,59 +2773,93 @@ } }, "node_modules/@babel/traverse": { - "version": "7.19.1", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.1", - "@babel/types": "^7.19.0", - "debug": "^4.1.0", - "globals": "^11.1.0" + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.19.0", + "node_modules/@babel/traverse/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.19.0", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@babel/types": { - "version": "7.19.0", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -4145,6 +2867,8 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { @@ -4156,6 +2880,8 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4165,6 +2891,8 @@ }, "node_modules/@csstools/postcss-cascade-layers": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4182,8 +2910,41 @@ "postcss": "^8.2" } }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@csstools/postcss-color-function": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4203,6 +2964,8 @@ }, "node_modules/@csstools/postcss-font-format-keywords": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4221,6 +2984,8 @@ }, "node_modules/@csstools/postcss-hwb-function": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4239,6 +3004,8 @@ }, "node_modules/@csstools/postcss-ic-unit": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4258,6 +3025,8 @@ }, "node_modules/@csstools/postcss-is-pseudo-class": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4275,8 +3044,41 @@ "postcss": "^8.2" } }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@csstools/postcss-nested-calc": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4295,6 +3097,8 @@ }, "node_modules/@csstools/postcss-normalize-display-values": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4313,6 +3117,8 @@ }, "node_modules/@csstools/postcss-oklab-function": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4332,6 +3138,8 @@ }, "node_modules/@csstools/postcss-progressive-custom-properties": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4346,6 +3154,8 @@ }, "node_modules/@csstools/postcss-stepped-value-functions": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4364,6 +3174,8 @@ }, "node_modules/@csstools/postcss-text-decoration-shorthand": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4382,6 +3194,8 @@ }, "node_modules/@csstools/postcss-trigonometric-functions": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -4400,6 +3214,8 @@ }, "node_modules/@csstools/postcss-unset-value": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", "dev": true, "license": "CC0-1.0", "engines": { @@ -4413,40 +3229,48 @@ "postcss": "^8.2" } }, - "node_modules/@csstools/selector-specificity": { - "version": "2.0.2", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "license": "CC0-1.0", + "license": "MIT", "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2", - "postcss-selector-parser": "^6.0.10" + "node": ">=10.0.0" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.0.0" + "node": ">=12" } }, "node_modules/@gar/promisify": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "devOptional": true, "license": "MIT" }, "node_modules/@ionic/angular": { - "version": "6.2.7", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-6.7.5.tgz", + "integrity": "sha512-nV8HP7RedjYkIAT8nVr5ifHNT0D3XzA74RPG3/WCCFJKunERNJ9SBiNkCTWhUpSkqsYYwEB4+SOOHz+R5NLk/w==", "license": "MIT", "dependencies": { - "@ionic/core": "^6.2.7", + "@ionic/core": "6.7.5", + "ionicons": "^6.1.3", "jsonc-parser": "^3.0.0", "tslib": "^2.0.0" }, @@ -4459,7 +3283,9 @@ } }, "node_modules/@ionic/cli": { - "version": "6.20.1", + "version": "6.20.9", + "resolved": "https://registry.npmjs.org/@ionic/cli/-/cli-6.20.9.tgz", + "integrity": "sha512-sItLCi7zXq1zARWIpZDinHhK8hvy+wzOx176QMOJV90BjDybkjGYu3rGu5TBjoqn104dRIZTC8rtCsnD/P3cQw==", "dev": true, "license": "MIT", "dependencies": { @@ -4499,6 +3325,8 @@ }, "node_modules/@ionic/cli-framework": { "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-5.1.3.tgz", + "integrity": "sha512-T2KN/TurzNoAcc3iDt1KHU6GeEa7x9kXngMnu5xs+DzJv5HhBKjVOoo74b8rgVxdPx+dLOV8aLrorlyvsHR/tQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4524,6 +3352,8 @@ }, "node_modules/@ionic/cli-framework-output": { "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.5.tgz", + "integrity": "sha512-YeDLTnTaE6V4IDUxT8GDIep0GuRIFaR7YZDLANMuuWJZDmnTku6DP+MmQoltBeLmVvz1BAAZgk41xzxdq6H2FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4537,6 +3367,8 @@ }, "node_modules/@ionic/cli-framework-prompts": { "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-prompts/-/cli-framework-prompts-2.1.10.tgz", + "integrity": "sha512-h8HbA0teR0vWtGKB3ahzRbDq4yYaxfukgbOqhu9CAEJHosoFlBmDB8PbPnGFYxUg2J1MuCqeiN2ftJQYV/BO1w==", "dev": true, "license": "MIT", "dependencies": { @@ -4549,228 +3381,55 @@ "node": ">=10.3.0" } }, - "node_modules/@ionic/cli-framework-prompts/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@ionic/cli-framework-prompts/node_modules/inquirer": { "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs": { - "version": "6.6.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/@ionic/cli-framework-prompts/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ionic/cli-framework/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@ionic/cli-framework/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@ionic/cli-framework/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@ionic/cli-framework/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@ionic/cli-framework/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@ionic/cli-framework/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ionic/cli/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@ionic/cli/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8.0.0" } }, - "node_modules/@ionic/cli/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "tslib": "^1.9.0" }, "engines": { - "node": ">=7.0.0" + "npm": ">=2.0.0" } }, - "node_modules/@ionic/cli/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@ionic/cli/node_modules/has-flag": { - "version": "4.0.0", + "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "0BSD" }, "node_modules/@ionic/cli/node_modules/open": { "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4784,28 +3443,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ionic/cli/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@ionic/core": { - "version": "6.2.7", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.7.5.tgz", + "integrity": "sha512-zRkRn+h/Vs3xt/EVgBdShMKDyeGOM4RU31NPF2icfu3CUTH+VrMV569MUnNjYvd1Lu2xK90pYy4TaicSWmC1Pw==", "license": "MIT", "dependencies": { - "@stencil/core": "^2.17.4", - "ionicons": "^6.0.3", + "@stencil/core": "^2.18.0", + "ionicons": "^6.1.3", "tslib": "^2.1.0" } }, "node_modules/@ionic/utils-array": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", "dev": true, "license": "MIT", "dependencies": { @@ -4818,6 +3470,8 @@ }, "node_modules/@ionic/utils-fs": { "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz", + "integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -4832,6 +3486,8 @@ }, "node_modules/@ionic/utils-network": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-network/-/utils-network-2.1.5.tgz", + "integrity": "sha512-HUQ1Ec4Mh2MXzzKdbbbDS6xYKwpFJ2XRY7SYXbaZT8+jiNahfHbsOfe62/p8bk41Yil7E9EagzGC2JvIFJh01w==", "dev": true, "license": "MIT", "dependencies": { @@ -4844,6 +3500,8 @@ }, "node_modules/@ionic/utils-object": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -4856,6 +3514,8 @@ }, "node_modules/@ionic/utils-process": { "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", "dev": true, "license": "MIT", "dependencies": { @@ -4872,6 +3532,8 @@ }, "node_modules/@ionic/utils-stream": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", "dev": true, "license": "MIT", "dependencies": { @@ -4884,6 +3546,8 @@ }, "node_modules/@ionic/utils-subprocess": { "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz", + "integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==", "dev": true, "license": "MIT", "dependencies": { @@ -4902,6 +3566,8 @@ }, "node_modules/@ionic/utils-terminal": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4921,6 +3587,8 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4936,6 +3604,8 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { @@ -4944,6 +3614,8 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", "dependencies": { @@ -4954,8 +3626,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { @@ -4964,6 +3645,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", "dev": true, "license": "MIT", "dependencies": { @@ -4975,7 +3658,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -4983,7 +3668,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", "engines": { @@ -4991,48 +3678,97 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true, "license": "MIT" }, + "node_modules/@maskito/angular": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-1.9.0.tgz", + "integrity": "sha512-Wa/9nM9Nv0oieVZ6yxQNXfDRA4obFDR15xO16o1GKF8i9W1IdQQn+tuMRjkmx6HhJDN9+x3k8OTJ1f80BIrhjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.6.2" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@maskito/core": "^1.9.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@maskito/angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/@maskito/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-1.9.0.tgz", + "integrity": "sha512-WQIUrwkdIUg6PzAb4Apa0RjTPHB0EqZLc9/7kWCKVIixhkITRFXFg2BhiDVSRv0mIKVlAEJcOvvjHK5T3UaIig==", + "license": "Apache-2.0" + }, + "node_modules/@maskito/kit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-1.9.0.tgz", + "integrity": "sha512-LNNgOJ0tAfrPoPehvoP+ZyYF9giOYL02sOMKyDC3IcqDNA8BAU0PARmS7TNsVEBpvSuJhU6xVt40nxJaONgUdw==", + "license": "Apache-2.0", + "peerDependencies": { + "@maskito/core": "^1.9.0" + } + }, "node_modules/@materia-ui/ngx-monaco-editor": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@materia-ui/ngx-monaco-editor/-/ngx-monaco-editor-6.0.0.tgz", + "integrity": "sha512-gTqNQjOGznZxOC0NlmKdKSGCJuTts8YmK4dsTQAGc5IgIV7cZdQWiW6AL742h0ruED6q0cAunEYjXT6jzHBoIQ==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -5043,8 +3779,10 @@ } }, "node_modules/@ng-web-apis/common": { - "version": "2.1.0", - "license": "MIT", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-3.2.3.tgz", + "integrity": "sha512-1ts2FkLRw6dE/uTuYFMf9VTbLJ9CS8dpfIXTpxFsPArs13mEuz0Yvpe0rl0tMAhfNoeN4e7V8wVSyqDNgfzgmw==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.2.0" }, @@ -5055,8 +3793,10 @@ } }, "node_modules/@ng-web-apis/intersection-observer": { - "version": "3.0.0", - "license": "MIT", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-3.2.3.tgz", + "integrity": "sha512-0yp+rr6ZEyF2vz4zYlMZ1GNtTHQziKajCurqAycZkSXVUdon7MhfiY/PiTr8xklTr40DjrF4anXV7oUAaA0szQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.2.0" }, @@ -5066,8 +3806,10 @@ } }, "node_modules/@ng-web-apis/mutation-observer": { - "version": "2.0.0", - "license": "MIT", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-3.2.3.tgz", + "integrity": "sha512-iFwxut1cw94lTXnloDMBRanqNTvEjbnigWkTowlPH3QY16ysTKuC51JeonRuARW4K3bbDGbXiLUYYZWKQaDfKw==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.2.0" }, @@ -5077,8 +3819,10 @@ } }, "node_modules/@ng-web-apis/resize-observer": { - "version": "2.0.0", - "license": "MIT", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-3.2.3.tgz", + "integrity": "sha512-x3KxBZSragzdQlkbY9tiHY0lVlkOFD7Y34rzXdBJ7PTnIvlq43X/tN31Mmb3R0Np4vsarWqnhO6Y42ljudsWdA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.2.0" }, @@ -5087,8 +3831,66 @@ "@ng-web-apis/common": ">=2.0.0" } }, + "node_modules/@ngtools/webpack": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.13.tgz", + "integrity": "sha512-RQx/rGX7K/+R55x1R6Ax1JzyeHi8cW11dEXpzHWipyuSpusQLUN53F02eMB4VTakXsL3mFNWWy4bX3/LSq8/9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^14.0.0", + "typescript": ">=4.6.2 <4.9", + "webpack": "^5.54.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5101,6 +3903,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "devOptional": true, "license": "MIT", "engines": { @@ -5109,6 +3913,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5121,6 +3927,8 @@ }, "node_modules/@npmcli/fs": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -5133,6 +3941,8 @@ }, "node_modules/@npmcli/git": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", + "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -5150,8 +3960,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@npmcli/installed-package-contents": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -5167,6 +3989,9 @@ }, "node_modules/@npmcli/move-file": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", "devOptional": true, "license": "MIT", "dependencies": { @@ -5179,6 +4004,8 @@ }, "node_modules/@npmcli/node-gyp": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-2.0.0.tgz", + "integrity": "sha512-doNI35wIe3bBaEgrlPfdJPaCpUR89pJWep4Hq3aRdh6gKazIVWfs0jHttvSSoq47ZXgC7h73kDsUl8AoIQUB+A==", "devOptional": true, "license": "ISC", "engines": { @@ -5187,6 +4014,8 @@ }, "node_modules/@npmcli/promise-spawn": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz", + "integrity": "sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g==", "devOptional": true, "license": "ISC", "dependencies": { @@ -5198,6 +4027,8 @@ }, "node_modules/@npmcli/run-script": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.1.tgz", + "integrity": "sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -5212,12 +4043,16 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.21", + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-json": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", "dev": true, "license": "MIT", "dependencies": { @@ -5229,6 +4064,8 @@ }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", + "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", "dev": true, "license": "MIT", "dependencies": { @@ -5248,6 +4085,8 @@ }, "node_modules/@rollup/pluginutils": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dev": true, "license": "MIT", "dependencies": { @@ -5264,16 +4103,19 @@ }, "node_modules/@rollup/pluginutils/node_modules/@types/estree": { "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true, "license": "MIT" }, "node_modules/@schematics/angular": { - "version": "14.2.3", - "devOptional": true, + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.2.13.tgz", + "integrity": "sha512-MLxTpTU3E8QACQ/5c0sENMR2gRiMXpGaKeD5IHY+3wyU2fUSJVB0QPU/l1WhoyZbX8N9ospBgf5UEG7taVF9rg==", "license": "MIT", "dependencies": { - "@angular-devkit/core": "14.2.3", - "@angular-devkit/schematics": "14.2.3", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", "jsonc-parser": "3.1.0" }, "engines": { @@ -5287,128 +4129,396 @@ "resolved": "https://registry.npmjs.org/@start9labs/argon2/-/argon2-0.2.2.tgz", "integrity": "sha512-OEJYDIicwwWg0NgG3d2GSO2Qs65B0LY9dIrlXFIJZJ1mo9vcDIU0kC2Yp8dg4XMt2U16ncsgru98s9I+y5Yuaw==" }, - "node_modules/@start9labs/emver": { - "version": "0.1.5", - "license": "MIT" + "node_modules/@start9labs/emver": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz", + "integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg==", + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk": { + "resolved": "../sdk/baseDist", + "link": true + }, + "node_modules/@stencil/core": { + "version": "2.22.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", + "integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=12.10.0", + "npm": ">=6.0.0" + } + }, + "node_modules/@taiga-ui/addon-charts": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.96.0.tgz", + "integrity": "sha512-vU8fZhwdg+sWOPJNnIrEtuuVkoFjQKeMhvk4f68oHBXUa0XUESS+qNeAfstvow6xnAZJrhxYtMoDuH/cgC7kNg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.7.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@ng-web-apis/common": ">=3.2.3 <4", + "@taiga-ui/cdk": ">=3.96.0 <4", + "@taiga-ui/core": ">=3.96.0 <4", + "@tinkoff/ng-polymorpheus": ">=4.3.0" + } + }, + "node_modules/@taiga-ui/addon-commerce": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.96.0.tgz", + "integrity": "sha512-Y1MACB6KrQVnNjgeKQrNfbz51jMXbU7j83sL28+6q8DS9LNv4DVcv/r/UvK2VZ1PhESkBU85NJAIHf5iy9GN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.7.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@maskito/angular": ">=1.9.0 <2", + "@maskito/core": ">=1.9.0 <2", + "@maskito/kit": ">=1.9.0 <2", + "@ng-web-apis/common": ">=3.2.3 <4", + "@taiga-ui/cdk": ">=3.96.0 <4", + "@taiga-ui/core": ">=3.96.0 <4", + "@taiga-ui/i18n": ">=3.96.0 <4", + "@taiga-ui/kit": ">=3.96.0 <4", + "@tinkoff/ng-polymorpheus": ">=4.3.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@taiga-ui/cdk": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.96.0.tgz", + "integrity": "sha512-y1T4+Olhys370ePT8SvZpMlOConDG6bLxjo6jOvFI6D6w0nEgqRxxFDBcdoHxgMWJZdAg7lRLEtN9dHEwKaABA==", + "license": "Apache-2.0", + "dependencies": { + "@ng-web-apis/common": "^3.2.3", + "@ng-web-apis/mutation-observer": "^3.2.3", + "@ng-web-apis/resize-observer": "^3.2.3", + "@tinkoff/ng-event-plugins": "^3.2.0", + "@tinkoff/ng-polymorpheus": "^4.3.0", + "tslib": "^2.7.0" + }, + "optionalDependencies": { + "ng-morph": "^4.8.2", + "parse5": "^6.0.1" + }, + "peerDependencies": { + "@angular/animations": ">=12.0.0", + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/@angular-devkit/core": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.4.tgz", + "integrity": "sha512-+imxIj1JLr2hbUYQePHgkTUKr0VmlxNSZvIREcCWtXUcdCypiwhJAtGXv6MfpB4hAx+FJZYEpVWeLwYOS/gW0A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@taiga-ui/cdk/node_modules/@angular-devkit/schematics": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.4.tgz", + "integrity": "sha512-2r6Qs4N5NSPho+qzegCYS8kIgylXyH4DHaS7HJ5+4XvM1I8V8AII8payLWkUK0i29XufVoD5XfPUFnjxZrBfYQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@angular-devkit/core": "19.0.4", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@taiga-ui/cdk/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT", + "optional": true }, - "node_modules/@stencil/core": { - "version": "2.18.0", + "node_modules/@taiga-ui/cdk/node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "license": "MIT", - "bin": { - "stencil": "bin/stencil" + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=12.10.0", - "npm": ">=6.0.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@taiga-ui/addon-charts": { - "version": "3.20.0", + "node_modules/@taiga-ui/cdk/node_modules/ng-morph": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-4.8.4.tgz", + "integrity": "sha512-XwL53wCOhyaAxvoekN74ONbWUK30huzp+GpZYyC01RfaG2AX9l7YlC1mGG/l7Rx7YXtFAk85VFnNJqn2e46K8g==", "license": "Apache-2.0", + "optional": true, "dependencies": { - "tslib": ">=2.0.0" + "jsonc-parser": "3.3.1", + "minimatch": "10.0.1", + "multimatch": "5.0.0", + "ts-morph": "23.0.0" }, "peerDependencies": { - "@angular/common": ">=12.0.0", - "@angular/core": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/core": ">=3.20.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0" + "@angular-devkit/core": ">=16.0.0", + "@angular-devkit/schematics": ">=16.0.0", + "tslib": "^2.7.0" } }, - "node_modules/@taiga-ui/cdk": { - "version": "3.20.0", + "node_modules/@taiga-ui/cdk/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@taiga-ui/core": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.96.0.tgz", + "integrity": "sha512-0w2Jpesb2oM+5aMTfSKFzzYdfPDHXq0fhE6TN4Eutc9LO4Lyh6Hg0KPfoHmw8Tj6wg4KxBcdeAj/opNehWwrVw==", "license": "Apache-2.0", "dependencies": { - "@ng-web-apis/common": "2.1.0", - "@ng-web-apis/mutation-observer": "2.0.0", - "@ng-web-apis/resize-observer": "2.0.0", - "@tinkoff/ng-event-plugins": "3.1.0", - "@tinkoff/ng-polymorpheus": "4.0.10", - "tslib": "2.5.0" - }, - "optionalDependencies": { - "ng-morph": "2.1.0", - "parse5": "6.0.1" + "@taiga-ui/i18n": "^3.96.0", + "tslib": ">=2.7.0" }, "peerDependencies": { "@angular/animations": ">=12.0.0", "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@angular/forms": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@angular/router": ">=12.0.0", + "@ng-web-apis/common": ">=3.2.3 <4", + "@ng-web-apis/mutation-observer": ">=3.2.3 <4", + "@taiga-ui/cdk": ">=3.96.0 <4", + "@taiga-ui/i18n": ">=3.96.0 <4", + "@tinkoff/ng-event-plugins": ">=3.2.0 <4", + "@tinkoff/ng-polymorpheus": ">=4.3.0", "rxjs": ">=6.0.0" } }, - "node_modules/@taiga-ui/cdk/node_modules/tslib": { - "version": "2.5.0", - "license": "0BSD" - }, - "node_modules/@taiga-ui/core": { - "version": "3.20.0", + "node_modules/@taiga-ui/experimental": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.96.0.tgz", + "integrity": "sha512-AxEaYieouK3NzkGzOVTojnHBAd/eRw5D2sVAK+SQdBFdBjQiRGk/FKzZhOWpDOQOcIN+lhpDGpth3KV30+AgFQ==", "license": "Apache-2.0", "dependencies": { - "@taiga-ui/i18n": "^3.20.0", - "tslib": ">=2.0.0" + "tslib": ">=2.7.0" }, "peerDependencies": { - "@angular/animations": ">=12.0.0", "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", - "@angular/forms": ">=12.0.0", - "@angular/platform-browser": ">=12.0.0", - "@angular/router": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/i18n": ">=3.20.0", - "@tinkoff/ng-event-plugins": ">=3.1.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0", + "@taiga-ui/addon-commerce": ">=3.96.0 <4", + "@taiga-ui/cdk": ">=3.96.0 <4", + "@taiga-ui/core": ">=3.96.0 <4", + "@taiga-ui/kit": ">=3.96.0 <4", + "@tinkoff/ng-polymorpheus": ">=4.3.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.20.0", + "version": "3.99.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.99.0.tgz", + "integrity": "sha512-2ZfdNXVIej+DZ4t7sZ1O87+FOOkZLFYFnoxqSZy+hOMPSqqXc5EvQza/mtQFzaOAOLk9Jd8nKJQQzmHQJbuDcw==", "license": "Apache-2.0", "dependencies": { - "tslib": ">=2.0.0" + "tslib": ">=2.7.0" }, "peerDependencies": { "@angular/core": ">=12.0.0", + "@ng-web-apis/common": ">=3.2.3 <4", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/icons": { - "version": "3.20.0", + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.96.0.tgz", + "integrity": "sha512-bBuX8RGAqr2+9TRbTA4RAmq3v4pnE9ObDwoq/5CrUtE3Wr7idcl7hgfW3yeyhpdOdJJMGfbTauPBONLz2uFRCQ==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.2.0" + "tslib": ">=2.7.0" + }, + "peerDependencies": { + "@taiga-ui/cdk": ">=3.96.0 <4" } }, "node_modules/@taiga-ui/kit": { - "version": "3.20.0", + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.96.0.tgz", + "integrity": "sha512-gHn0AZU1kiNZU2T/LnnxLGkHC3/XNroBWCGmhxupAmL/JcsmOXsl3L8aK1rZhjS4QgkmgaC5ueVWNwdXINRnXw==", "license": "Apache-2.0", "dependencies": { - "@ng-web-apis/intersection-observer": "3.0.0", - "text-mask-core": "5.1.2", - "tslib": ">=2.0.0" + "@maskito/angular": "^1.9.0", + "@maskito/core": "^1.9.0", + "@maskito/kit": "^1.9.0", + "@ng-web-apis/intersection-observer": "^3.2.3", + "text-mask-core": "^5.1.2", + "tslib": ">=2.7.0" }, "peerDependencies": { "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@angular/forms": ">=12.0.0", "@angular/router": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/core": ">=3.20.0", - "@taiga-ui/i18n": ">=3.20.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0", + "@ng-web-apis/common": ">=3.2.3 <4", + "@ng-web-apis/mutation-observer": ">=3.2.3 <4", + "@ng-web-apis/resize-observer": ">=3.2.3 <4", + "@taiga-ui/cdk": ">=3.96.0 <4", + "@taiga-ui/core": ">=3.96.0 <4", + "@taiga-ui/i18n": ">=3.96.0 <4", + "@tinkoff/ng-polymorpheus": ">=4.3.0", "rxjs": ">=6.0.0" } }, + "node_modules/@tinkoff/ng-dompurify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-dompurify/-/ng-dompurify-4.0.0.tgz", + "integrity": "sha512-BjKUweWLrOx8UOZw+Tl+Dae5keYuSbeMkppcXQdsvwASMrPfmP7d3Q206Q6HDqOV2WnpnFqGUB95IMbLAeRRuw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@types/dompurify": ">=2.3.0", + "dompurify": ">= 2.3.0" + } + }, "node_modules/@tinkoff/ng-event-plugins": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-event-plugins/-/ng-event-plugins-3.2.0.tgz", + "integrity": "sha512-n56R5xNfiytabh2WmWdQXfNU6m7dfOo3LLxlARE+DX7f5yciW2xBdDkuEHX74q8dlCuAVlW9aslSfz8c//ymwA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.2.0" @@ -5420,17 +4530,28 @@ } }, "node_modules/@tinkoff/ng-polymorpheus": { - "version": "4.0.10", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-polymorpheus/-/ng-polymorpheus-4.3.0.tgz", + "integrity": "sha512-Ck/XCLuBwlUgvK22PxTlLTZhGG6I32kLqLYtDQh8N/QZZhs40+hb/78/ElFGzD567CCvrzNnueFkaOoXhuEVrw==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.0.0" + "tslib": "2.6.2" }, "peerDependencies": { - "@angular/core": ">=12.0.0" + "@angular/core": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0" } }, + "node_modules/@tinkoff/ng-polymorpheus/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "devOptional": true, "license": "MIT", "engines": { @@ -5438,58 +4559,82 @@ } }, "node_modules/@ts-morph/common": { - "version": "0.9.2", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", + "integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==", "license": "MIT", "optional": true, "dependencies": { - "fast-glob": "^3.2.5", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", + "fast-glob": "^3.3.2", + "minimatch": "^9.0.4", + "mkdirp": "^3.0.1", "path-browserify": "^1.0.1" } }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "optional": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "bin": { + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, "license": "MIT" }, "node_modules/@types/body-parser": { - "version": "1.19.2", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "license": "MIT", "dependencies": { @@ -5498,7 +4643,9 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5506,7 +4653,9 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { @@ -5514,7 +4663,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "license": "MIT", "dependencies": { @@ -5523,15 +4674,18 @@ } }, "node_modules/@types/dompurify": { - "version": "2.3.4", - "dev": true, + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", "license": "MIT", "dependencies": { "@types/trusted-types": "*" } }, "node_modules/@types/eslint": { - "version": "8.4.6", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "dependencies": { @@ -5540,7 +4694,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", "dependencies": { @@ -5550,40 +4706,71 @@ }, "node_modules/@types/estree": { "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true, "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.14", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/fs-extra": { - "version": "8.1.2", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-proxy": { - "version": "1.17.9", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5591,42 +4778,68 @@ } }, "node_modules/@types/js-yaml": { - "version": "4.0.5", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { - "version": "7.0.11", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/marked": { - "version": "4.0.7", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", "dev": true, "license": "MIT" }, "node_modules/@types/mime": { - "version": "3.0.1", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, "node_modules/@types/minimatch": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "license": "MIT", "optional": true }, "node_modules/@types/mustache": { - "version": "4.2.1", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "16.11.59", + "version": "16.18.121", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.121.tgz", + "integrity": "sha512-Gk/pOy8H0cvX8qNrwzElYIECpcUn87w4EAEFXFvPJ8qsP9QR/YqukUORSy0zmyDyvdo149idPpy4W6iC5aSbQA==", "dev": true, "license": "MIT" }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node-jose": { - "version": "1.1.10", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.13.tgz", + "integrity": "sha512-QjMd4yhwy1EvSToQn0YI3cD29YhyfxFwj7NecuymjLys2/P0FwxWnkgBlFxCai6Y3aBCe7rbwmqwJJawxlgcXw==", "dev": true, "license": "MIT", "dependencies": { @@ -5634,12 +4847,16 @@ } }, "node_modules/@types/parse-json": { - "version": "4.0.0", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true, "license": "MIT" }, "node_modules/@types/pbkdf2": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", "dev": true, "license": "MIT", "dependencies": { @@ -5647,17 +4864,23 @@ } }, "node_modules/@types/qs": { - "version": "6.9.7", + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { - "version": "1.2.4", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", "dev": true, "license": "MIT", "dependencies": { @@ -5666,11 +4889,26 @@ }, "node_modules/@types/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true, "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/serve-index": { - "version": "1.9.1", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "license": "MIT", "dependencies": { @@ -5678,21 +4916,28 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.0", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/slice-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", "dev": true, "license": "MIT" }, "node_modules/@types/sockjs": { - "version": "0.3.33", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5700,17 +4945,22 @@ } }, "node_modules/@types/trusted-types": { - "version": "2.0.2", - "dev": true, + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/@types/uuid": { "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.3", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "license": "MIT", "dependencies": { @@ -5718,163 +4968,221 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "devOptional": true, "license": "BSD-2-Clause" }, "node_modules/abab": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "devOptional": true, "license": "ISC" }, "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { @@ -5885,8 +5193,20 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.8.0", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -5897,7 +5217,10 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "deprecated": "package has been renamed to acorn-import-attributes", "dev": true, "license": "MIT", "peerDependencies": { @@ -5905,15 +5228,22 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, "license": "MIT", "dependencies": { @@ -5926,6 +5256,8 @@ }, "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -5939,6 +5271,8 @@ }, "node_modules/agent-base": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5949,12 +5283,12 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "devOptional": true, "license": "MIT", "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -5963,6 +5297,8 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5975,6 +5311,8 @@ }, "node_modules/ajv": { "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5989,6 +5327,8 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -6004,6 +5344,8 @@ }, "node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "dependencies": { @@ -6015,6 +5357,8 @@ }, "node_modules/angular-svg-round-progressbar": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz", + "integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -6026,6 +5370,8 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "devOptional": true, "license": "MIT", "engines": { @@ -6034,6 +5380,8 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6048,6 +5396,8 @@ }, "node_modules/ansi-html-community": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, "engines": [ "node >= 0.8.0" @@ -6059,24 +5409,32 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/ansi-to-html": { "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", "license": "MIT", "dependencies": { "entities": "^2.2.0" @@ -6089,7 +5447,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -6102,11 +5462,16 @@ }, "node_modules/aproba": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "devOptional": true, "license": "ISC" }, "node_modules/are-we-there-yet": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "devOptional": true, "license": "ISC", "dependencies": { @@ -6119,15 +5484,21 @@ }, "node_modules/arg": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-differ": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "license": "MIT", "optional": true, "engines": { @@ -6135,12 +5506,16 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true, "license": "MIT" }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "license": "MIT", "optional": true, "engines": { @@ -6149,6 +5524,8 @@ }, "node_modules/arrify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "license": "MIT", "optional": true, "engines": { @@ -6157,6 +5534,8 @@ }, "node_modules/ast-types": { "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, "license": "MIT", "dependencies": { @@ -6168,6 +5547,8 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "engines": { @@ -6176,11 +5557,15 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { @@ -6189,6 +5574,8 @@ }, "node_modules/atob": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true, "license": "(MIT OR Apache-2.0)", "bin": { @@ -6199,7 +5586,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.11", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -6209,15 +5598,19 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "browserslist": "^4.21.3", - "caniuse-lite": "^1.0.30001399", - "fraction.js": "^4.2.0", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -6232,6 +5625,8 @@ }, "node_modules/babel-loader": { "version": "8.2.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", + "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6250,6 +5645,8 @@ }, "node_modules/babel-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -6261,16 +5658,10 @@ "node": ">=8.9.0" } }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6286,6 +5677,8 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6298,7 +5691,9 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -6307,6 +5702,8 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", "dev": true, "license": "MIT", "dependencies": { @@ -6319,6 +5716,8 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", "dev": true, "license": "MIT", "dependencies": { @@ -6330,11 +5729,15 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -6353,6 +5756,8 @@ }, "node_modules/base64url": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -6360,11 +5765,15 @@ }, "node_modules/batch": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true, "license": "MIT" }, "node_modules/big.js": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, "license": "MIT", "engines": { @@ -6372,15 +5781,22 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "devOptional": true, "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -6390,6 +5806,8 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -6411,20 +5829,22 @@ } }, "node_modules/body-parser": { - "version": "1.20.0", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -6435,31 +5855,29 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/body-parser/node_modules/qs": { - "version": "6.10.3", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6469,23 +5887,27 @@ } }, "node_modules/bonjour-service": { - "version": "1.0.14", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "node_modules/boolbase": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6493,18 +5915,22 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "devOptional": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.21.4", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -6514,14 +5940,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -6532,6 +5962,8 @@ }, "node_modules/buffer": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -6554,11 +5986,15 @@ }, "node_modules/buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/builtin-modules": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, "license": "MIT", "engines": { @@ -6569,7 +6005,9 @@ } }, "node_modules/builtins": { - "version": "5.0.1", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6578,6 +6016,8 @@ }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -6586,6 +6026,8 @@ }, "node_modules/cacache": { "version": "16.1.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", + "integrity": "sha512-Xx+xPlfCZIUHagysjjOAje9nRo8pRDczQCcXb4J2O0BLtH+xeVue6ba4y1kfJfQMAnM2mkcoMIAyOctlaRGWYA==", "devOptional": true, "license": "ISC", "dependencies": { @@ -6612,20 +6054,53 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { - "version": "1.0.2", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.0.tgz", + "integrity": "sha512-CCKAP2tkPau7D3GE8+V8R6sQubA9R5foIzGp+85EXCVSCivuxBNAWqcpn72PKYiIcqoViv/kcUDpaEIMBVi1lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -6634,13 +6109,17 @@ }, "node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001407", + "version": "1.0.30001687", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz", + "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==", "dev": true, "funding": [ { @@ -6650,6 +6129,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "CC-BY-4.0" @@ -6657,42 +6140,47 @@ "node_modules/cbor": { "name": "@jprochazk/cbor", "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@jprochazk/cbor/-/cbor-0.4.9.tgz", + "integrity": "sha512-FWNnkOtWrFOLXKG2nzOHR/EnCCGZZPvatAvWXDmkTDxgjj9JHDK3DkMUHcFCY3a9weylMCSO/nLOUM170NAO0Q==", "license": "MIT" }, "node_modules/cbor-web": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz", + "integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==", "license": "MIT", "engines": { "node": ">=12.19" } }, "node_modules/chalk": { - "version": "2.4.2", - "dev": true, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chardet": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "devOptional": true, "license": "MIT" }, "node_modules/chokidar": { - "version": "3.5.3", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -6706,12 +6194,17 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/chownr": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "devOptional": true, "license": "ISC", "engines": { @@ -6719,7 +6212,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", "engines": { @@ -6728,19 +6223,28 @@ }, "node_modules/ci-info": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true, "license": "MIT" }, "node_modules/cipher-base": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/clean-stack": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "devOptional": true, "license": "MIT", "engines": { @@ -6749,6 +6253,8 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" @@ -6758,7 +6264,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.7.0", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { "node": ">=6" @@ -6769,6 +6277,8 @@ }, "node_modules/cli-truncate": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", "dev": true, "license": "MIT", "dependencies": { @@ -6783,7 +6293,9 @@ } }, "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -6794,7 +6306,9 @@ } }, "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.1.1", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { @@ -6806,11 +6320,15 @@ }, "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, "license": "MIT", "engines": { @@ -6822,6 +6340,8 @@ }, "node_modules/cli-truncate/node_modules/slice-ansi": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6837,6 +6357,8 @@ }, "node_modules/cli-truncate/node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -6852,7 +6374,9 @@ } }, "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.0.1", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6867,6 +6391,8 @@ }, "node_modules/cli-width": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "devOptional": true, "license": "ISC", "engines": { @@ -6875,6 +6401,8 @@ }, "node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -6885,6 +6413,8 @@ }, "node_modules/clone": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", "engines": { "node": ">=0.8" @@ -6892,6 +6422,8 @@ }, "node_modules/clone-deep": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6904,25 +6436,34 @@ } }, "node_modules/code-block-writer": { - "version": "10.1.1", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT", "optional": true }, "node_modules/color-convert": { - "version": "1.9.3", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-support": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "devOptional": true, "license": "ISC", "bin": { @@ -6930,12 +6471,16 @@ } }, "node_modules/colorette": { - "version": "2.0.19", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -6946,7 +6491,9 @@ } }, "node_modules/commander": { - "version": "9.4.0", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", "engines": { @@ -6955,21 +6502,32 @@ }, "node_modules/commondir": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true, "license": "MIT" }, "node_modules/compare-versions": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", "dev": true, "license": "MIT" }, "node_modules/component-emitter": { - "version": "1.3.0", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compressible": { "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "license": "MIT", "dependencies": { @@ -6980,32 +6538,28 @@ } }, "node_modules/compression": { - "version": "1.7.4", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -7014,16 +6568,22 @@ }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "devOptional": true, "license": "MIT" }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, "license": "MIT", "engines": { @@ -7032,11 +6592,15 @@ }, "node_modules/console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "devOptional": true, "license": "ISC" }, "node_modules/content-disposition": { "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7046,27 +6610,10 @@ "node": ">= 0.6" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/content-type": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -7074,15 +6621,16 @@ } }, "node_modules/convert-source-map": { - "version": "1.8.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.1" - } + "license": "MIT" }, "node_modules/cookie": { - "version": "0.5.0", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -7091,16 +6639,22 @@ }, "node_modules/cookie-signature": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true, "license": "MIT" }, "node_modules/cookiejar": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, "license": "MIT" }, "node_modules/copy-anything": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", "dev": true, "license": "MIT", "dependencies": { @@ -7112,6 +6666,8 @@ }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7135,6 +6691,8 @@ }, "node_modules/copy-webpack-plugin/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -7145,14 +6703,16 @@ } }, "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -7163,7 +6723,9 @@ } }, "node_modules/core-js": { - "version": "3.25.2", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -7172,11 +6734,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.25.2", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.21.4" + "browserslist": "^4.24.2" }, "funding": { "type": "opencollective", @@ -7185,11 +6749,15 @@ }, "node_modules/core-util-is": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { - "version": "7.0.1", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, "license": "MIT", "dependencies": { @@ -7205,6 +6773,8 @@ }, "node_modules/create-hash": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", @@ -7216,6 +6786,8 @@ }, "node_modules/create-hmac": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "license": "MIT", "dependencies": { "cipher-base": "^1.0.3", @@ -7228,11 +6800,15 @@ }, "node_modules/create-require": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, "license": "MIT" }, "node_modules/critters": { "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7244,72 +6820,10 @@ "pretty-bytes": "^5.3.0" } }, - "node_modules/critters/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/critters/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/critters/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/critters/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/critters/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/critters/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -7323,6 +6837,8 @@ }, "node_modules/css-blank-pseudo": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -7338,8 +6854,24 @@ "postcss": "^8.4" } }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-has-pseudo": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -7355,8 +6887,24 @@ "postcss": "^8.4" } }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-loader": { "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", "dev": true, "license": "MIT", "dependencies": { @@ -7382,6 +6930,8 @@ }, "node_modules/css-prefers-color-scheme": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", "dev": true, "license": "CC0-1.0", "bin": { @@ -7396,6 +6946,8 @@ }, "node_modules/css-select": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7411,6 +6963,8 @@ }, "node_modules/css-what": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7421,16 +6975,26 @@ } }, "node_modules/cssdb": { - "version": "7.0.1", + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", + "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", "dev": true, - "license": "CC0-1.0", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "CC0-1.0" }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { @@ -7442,19 +7006,32 @@ }, "node_modules/cuint": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", "dev": true, "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7471,6 +7048,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7478,6 +7057,8 @@ }, "node_modules/decode-uri-component": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, "license": "MIT", "engines": { @@ -7486,11 +7067,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { - "version": "4.2.2", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { @@ -7499,6 +7084,8 @@ }, "node_modules/default-gateway": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7509,60 +7096,56 @@ } }, "node_modules/defaults": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "license": "MIT", "dependencies": { "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "devOptional": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/degenerator": { - "version": "3.0.2", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.4.tgz", + "integrity": "sha512-Z66uPeBfHZAHVmue3HPfyKu2Q0rC2cRxbTOsvmU/po5fvvcx27W4mIu9n0PUlQih4oUYvcG1BsbtVv8x7KDOSw==", "dev": true, "license": "MIT", "dependencies": { "ast-types": "^0.13.2", "escodegen": "^1.8.1", "esprima": "^4.0.0", - "vm2": "^3.9.8" + "vm2": "^3.9.17" }, "engines": { "node": ">= 6" @@ -7570,6 +7153,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -7578,19 +7163,25 @@ }, "node_modules/delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "devOptional": true, "license": "MIT" }, "node_modules/depd": { - "version": "1.1.2", - "devOptional": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/dependency-graph": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true, "license": "MIT", "engines": { @@ -7599,6 +7190,8 @@ }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", "engines": { @@ -7608,11 +7201,15 @@ }, "node_modules/detect-node": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT" }, "node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7620,11 +7217,15 @@ } }, "node_modules/dijkstrajs": { - "version": "1.0.2", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { @@ -7634,13 +7235,10 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.4.0", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "license": "MIT", "dependencies": { @@ -7652,6 +7250,8 @@ }, "node_modules/dom-serializer": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, "license": "MIT", "dependencies": { @@ -7664,7 +7264,9 @@ } }, "node_modules/dom7": { - "version": "4.0.4", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz", + "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==", "license": "MIT", "dependencies": { "ssr-window": "^4.0.0" @@ -7672,6 +7274,8 @@ }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -7683,6 +7287,8 @@ }, "node_modules/domhandler": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7696,11 +7302,15 @@ } }, "node_modules/dompurify": { - "version": "2.4.0", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz", + "integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==", "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/domutils": { "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7712,245 +7322,659 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2": { - "version": "0.1.4", + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.71", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz", + "integrity": "sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.7", + "node_modules/esbuild-android-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", + "node_modules/esbuild-darwin-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/electron-to-chromium": { - "version": "1.4.255", + "node_modules/esbuild-freebsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/elementtree": { - "version": "0.1.7", + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "sax": "1.1.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.4.0" + "node": ">=12" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", + "node_modules/esbuild-linux-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 4" + "node": ">=12" } }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", + "node_modules/esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/encoding": { - "version": "0.1.13", + "node_modules/esbuild-linux-arm": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", + "node_modules/esbuild-linux-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/enhanced-resolve": { - "version": "5.10.0", + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.13.0" + "node": ">=12" } }, - "node_modules/entities": { - "version": "2.2.0", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "devOptional": true, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/err-code": { - "version": "2.0.3", - "devOptional": true, - "license": "MIT" - }, - "node_modules/errno": { - "version": "0.1.8", + "node_modules/esbuild-netbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/error-ex": { - "version": "1.3.2", + "node_modules/esbuild-openbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/es-module-lexer": { - "version": "0.9.3", + "node_modules/esbuild-sunos-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/es6-promise": { - "version": "4.2.8", - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/esbuild": { + "node_modules/esbuild-wasm": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.15.5.tgz", + "integrity": "sha512-lTJOEKekN/4JI/eOEq0wLcx53co2N6vaT/XjBz46D1tvIVoUEyM0o2K6txW6gEotf31szFD/J1PbxmnbkGlK9A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/linux-loong64": "0.15.5", - "esbuild-android-64": "0.15.5", - "esbuild-android-arm64": "0.15.5", - "esbuild-darwin-64": "0.15.5", - "esbuild-darwin-arm64": "0.15.5", - "esbuild-freebsd-64": "0.15.5", - "esbuild-freebsd-arm64": "0.15.5", - "esbuild-linux-32": "0.15.5", - "esbuild-linux-64": "0.15.5", - "esbuild-linux-arm": "0.15.5", - "esbuild-linux-arm64": "0.15.5", - "esbuild-linux-mips64le": "0.15.5", - "esbuild-linux-ppc64le": "0.15.5", - "esbuild-linux-riscv64": "0.15.5", - "esbuild-linux-s390x": "0.15.5", - "esbuild-netbsd-64": "0.15.5", - "esbuild-openbsd-64": "0.15.5", - "esbuild-sunos-64": "0.15.5", - "esbuild-windows-32": "0.15.5", - "esbuild-windows-64": "0.15.5", - "esbuild-windows-arm64": "0.15.5" } }, - "node_modules/esbuild-darwin-arm64": { + "node_modules/esbuild-windows-32": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/esbuild-wasm": { + "node_modules/esbuild-windows-64": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=12" } }, "node_modules/escalade": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "devOptional": true, "license": "MIT", "engines": { @@ -7959,11 +7983,15 @@ }, "node_modules/escape-html": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "devOptional": true, "license": "MIT", "engines": { @@ -7972,6 +8000,8 @@ }, "node_modules/escodegen": { "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7993,6 +8023,8 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -8002,6 +8034,8 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8014,6 +8048,8 @@ }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", "bin": { @@ -8026,6 +8062,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8037,6 +8075,8 @@ }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -8045,6 +8085,8 @@ }, "node_modules/estraverse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -8053,11 +8095,15 @@ }, "node_modules/estree-walker": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "dev": true, "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -8066,6 +8112,8 @@ }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -8074,16 +8122,22 @@ }, "node_modules/eventemitter-asyncresource": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", "dev": true, "license": "MIT" }, "node_modules/eventemitter3": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT" }, "node_modules/events": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "engines": { @@ -8092,6 +8146,8 @@ }, "node_modules/execa": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { @@ -8112,37 +8168,46 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/express": { - "version": "4.18.1", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8151,40 +8216,37 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/express/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/express/node_modules/qs": { - "version": "6.10.3", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -8193,27 +8255,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/external-editor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8228,10 +8273,13 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.2.12", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8247,25 +8295,43 @@ }, "node_modules/fast-json-patch": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, "node_modules/fastq": { - "version": "1.13.0", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -8274,6 +8340,8 @@ }, "node_modules/faye-websocket": { "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8285,6 +8353,8 @@ }, "node_modules/figures": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8299,6 +8369,8 @@ }, "node_modules/file-uri-to-path": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", "dev": true, "license": "MIT", "engines": { @@ -8306,7 +8378,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8317,12 +8391,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8335,6 +8411,8 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -8343,11 +8421,15 @@ }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/find-cache-dir": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "license": "MIT", "dependencies": { @@ -8364,6 +8446,8 @@ }, "node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -8375,6 +8459,8 @@ }, "node_modules/find-versions": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", + "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8388,7 +8474,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -8407,7 +8495,9 @@ } }, "node_modules/form-data": { - "version": "3.0.1", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", + "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8421,6 +8511,9 @@ }, "node_modules/formidable": { "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", "dev": true, "license": "MIT", "funding": { @@ -8429,6 +8522,8 @@ }, "node_modules/forwarded": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -8436,7 +8531,9 @@ } }, "node_modules/fraction.js": { - "version": "4.2.0", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "license": "MIT", "engines": { @@ -8444,11 +8541,13 @@ }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, "license": "MIT", "engines": { @@ -8457,6 +8556,8 @@ }, "node_modules/fs-extra": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8471,6 +8572,8 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -8481,18 +8584,25 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "dev": true, "license": "Unlicense" }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.2", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -8504,6 +8614,8 @@ }, "node_modules/ftp": { "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", "dev": true, "dependencies": { "readable-stream": "1.1.x", @@ -8515,11 +8627,15 @@ }, "node_modules/ftp/node_modules/isarray": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true, "license": "MIT" }, "node_modules/ftp/node_modules/readable-stream": { "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8531,16 +8647,25 @@ }, "node_modules/ftp/node_modules/string_decoder": { "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true, "license": "MIT" }, "node_modules/function-bind": { - "version": "1.1.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/fuse.js": { "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", "license": "Apache-2.0", "engines": { "node": ">=10" @@ -8548,6 +8673,9 @@ }, "node_modules/gauge": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "devOptional": true, "license": "ISC", "dependencies": { @@ -8566,6 +8694,8 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -8574,21 +8704,31 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8596,6 +8736,8 @@ }, "node_modules/get-package-type": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { @@ -8604,6 +8746,8 @@ }, "node_modules/get-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", "engines": { @@ -8615,6 +8759,8 @@ }, "node_modules/get-uri": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", "dev": true, "license": "MIT", "dependencies": { @@ -8631,6 +8777,8 @@ }, "node_modules/get-uri/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, "license": "MIT", "engines": { @@ -8639,6 +8787,8 @@ }, "node_modules/get-uri/node_modules/fs-extra": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -8652,6 +8802,8 @@ }, "node_modules/get-uri/node_modules/jsonfile": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -8660,6 +8812,8 @@ }, "node_modules/get-uri/node_modules/universalify": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -8668,6 +8822,9 @@ }, "node_modules/glob": { "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, "license": "ISC", "dependencies": { @@ -8686,6 +8843,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "devOptional": true, "license": "ISC", "dependencies": { @@ -8697,11 +8856,15 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/globals": { "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "license": "MIT", "engines": { @@ -8709,13 +8872,15 @@ } }, "node_modules/globby": { - "version": "13.1.2", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", "dev": true, "license": "MIT", "dependencies": { "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" }, @@ -8727,24 +8892,29 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { - "version": "4.2.10", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "devOptional": true, "license": "ISC" }, "node_modules/gzip-size": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8759,53 +8929,37 @@ }, "node_modules/handle-thing": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true, "license": "MIT" }, - "node_modules/has": { - "version": "1.0.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, - "engines": { - "node": ">= 0.4" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -8817,11 +8971,15 @@ }, "node_modules/has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "devOptional": true, "license": "ISC" }, "node_modules/hash-base": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -8832,26 +8990,23 @@ "node": ">=4" } }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/hdr-histogram-js": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", "dev": true, "license": "BSD", "dependencies": { @@ -8862,16 +9017,22 @@ }, "node_modules/hdr-histogram-js/node_modules/pako": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/hdr-histogram-percentiles-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", "dev": true, "license": "MIT" }, "node_modules/he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "license": "MIT", "bin": { @@ -8879,7 +9040,9 @@ } }, "node_modules/hosted-git-info": { - "version": "5.1.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", + "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -8889,8 +9052,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/hpack.js": { "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8901,7 +9076,9 @@ } }, "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.7", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -8914,8 +9091,17 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { @@ -8923,22 +9109,47 @@ } }, "node_modules/html-entities": { - "version": "2.3.3", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/http-cache-semantics": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "devOptional": true, "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "dev": true, "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8952,21 +9163,17 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-parser-js": { "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", "dev": true, "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8980,6 +9187,8 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8992,7 +9201,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -9016,6 +9227,8 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9028,6 +9241,8 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9036,6 +9251,8 @@ }, "node_modules/humanize-ms": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9044,6 +9261,8 @@ }, "node_modules/husky": { "version": "4.3.8", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", + "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9068,56 +9287,13 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/husky" - } - }, - "node_modules/husky/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/husky/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/husky/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "url": "https://opencollective.com/husky" } }, - "node_modules/husky/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/husky/node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -9131,16 +9307,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/husky/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/husky/node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -9155,6 +9325,8 @@ }, "node_modules/husky/node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9169,6 +9341,8 @@ }, "node_modules/husky/node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -9183,6 +9357,8 @@ }, "node_modules/husky/node_modules/pkg-dir": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, "license": "MIT", "dependencies": { @@ -9194,25 +9370,18 @@ }, "node_modules/husky/node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/husky/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9224,6 +9393,8 @@ }, "node_modules/icss-utils": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, "license": "ISC", "engines": { @@ -9235,6 +9406,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -9252,7 +9425,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.2.0", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -9261,6 +9436,8 @@ }, "node_modules/ignore-walk": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", + "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -9272,6 +9449,8 @@ }, "node_modules/image-size": { "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, "license": "MIT", "optional": true, @@ -9283,12 +9462,16 @@ } }, "node_modules/immutable": { - "version": "4.1.0", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "dev": true, "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9304,6 +9487,8 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -9312,6 +9497,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "devOptional": true, "license": "MIT", "engines": { @@ -9320,6 +9507,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "devOptional": true, "license": "MIT", "engines": { @@ -9328,11 +9517,16 @@ }, "node_modules/infer-owner": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", "devOptional": true, "license": "ISC" }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "devOptional": true, "license": "ISC", "dependencies": { @@ -9342,10 +9536,14 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.0.tgz", + "integrity": "sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw==", "devOptional": true, "license": "ISC", "engines": { @@ -9354,6 +9552,8 @@ }, "node_modules/injection-js": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", "dev": true, "license": "MIT", "dependencies": { @@ -9362,6 +9562,8 @@ }, "node_modules/inquirer": { "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9385,95 +9587,40 @@ "node": ">=12.0.0" } }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "devOptional": true, + "node_modules/ionicons": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz", + "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "@stencil/core": "^2.18.0" } }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "devOptional": true, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "dev": true, "license": "MIT" }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "devOptional": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ionicons": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "@stencil/core": "~2.16.0" - } - }, - "node_modules/ionicons/node_modules/@stencil/core": { - "version": "2.16.1", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" }, "engines": { - "node": ">=12.10.0", - "npm": ">=6.0.0" + "node": ">= 12" } }, - "node_modules/ip": { - "version": "2.0.0", - "devOptional": true, - "license": "MIT" - }, "node_modules/ipaddr.js": { - "version": "2.0.1", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, "license": "MIT", "engines": { @@ -9482,11 +9629,15 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9497,7 +9648,9 @@ } }, "node_modules/is-builtin-module": { - "version": "3.2.0", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "license": "MIT", "dependencies": { @@ -9511,11 +9664,16 @@ } }, "node_modules/is-core-module": { - "version": "2.10.0", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "devOptional": true, "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9523,6 +9681,8 @@ }, "node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "devOptional": true, "license": "MIT", "bin": { @@ -9537,6 +9697,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "devOptional": true, "license": "MIT", "engines": { @@ -9545,6 +9707,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -9552,6 +9716,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9563,6 +9729,8 @@ }, "node_modules/is-interactive": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { "node": ">=8" @@ -9570,16 +9738,22 @@ }, "node_modules/is-lambda": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "devOptional": true, "license": "MIT" }, "node_modules/is-module": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "devOptional": true, "license": "MIT", "engines": { @@ -9588,6 +9762,8 @@ }, "node_modules/is-plain-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", "engines": { @@ -9599,6 +9775,8 @@ }, "node_modules/is-plain-object": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "license": "MIT", "dependencies": { @@ -9610,6 +9788,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { @@ -9621,11 +9801,15 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true, "license": "MIT" }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "license": "MIT", "engines": { "node": ">=10" @@ -9636,11 +9820,15 @@ }, "node_modules/is-what": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", "dev": true, "license": "MIT" }, "node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9652,16 +9840,22 @@ }, "node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "devOptional": true, "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "license": "MIT", "engines": { @@ -9669,7 +9863,9 @@ } }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -9677,7 +9873,9 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9692,7 +9890,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -9701,6 +9901,8 @@ }, "node_modules/jest-worker": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { @@ -9712,16 +9914,10 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9735,7 +9931,9 @@ } }, "node_modules/jose": { - "version": "4.9.3", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -9743,11 +9941,15 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9756,8 +9958,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "devOptional": true, + "license": "MIT" + }, "node_modules/jsesc": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, "license": "MIT", "bin": { @@ -9769,15 +9980,21 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "devOptional": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -9789,10 +10006,14 @@ }, "node_modules/jsonc-parser": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9804,6 +10025,8 @@ }, "node_modules/jsonparse": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "devOptional": true, "engines": [ "node >= 0.2.0" @@ -9812,6 +10035,8 @@ }, "node_modules/karma-source-map-support": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, "license": "MIT", "dependencies": { @@ -9820,6 +10045,8 @@ }, "node_modules/kind-of": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", "engines": { @@ -9827,7 +10054,9 @@ } }, "node_modules/klona": { - "version": "2.0.5", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "dev": true, "license": "MIT", "engines": { @@ -9836,6 +10065,8 @@ }, "node_modules/leek": { "version": "0.0.24", + "resolved": "https://registry.npmjs.org/leek/-/leek-0.0.24.tgz", + "integrity": "sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9846,6 +10077,8 @@ }, "node_modules/leek/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -9854,11 +10087,15 @@ }, "node_modules/leek/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/less": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9884,6 +10121,8 @@ }, "node_modules/less-loader": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.0.0.tgz", + "integrity": "sha512-9+LOWWjuoectIEx3zrfN83NAGxSUB5pWEabbbidVQVgZhN+wN68pOvuyirVlH1IK4VT1f3TmlyvAnCXh8O5KEw==", "dev": true, "license": "MIT", "dependencies": { @@ -9903,6 +10142,8 @@ }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "license": "MIT", "optional": true, @@ -9914,8 +10155,24 @@ "node": ">=6" } }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/less/node_modules/pify": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "license": "MIT", "optional": true, @@ -9924,7 +10181,9 @@ } }, "node_modules/less/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "optional": true, @@ -9934,6 +10193,8 @@ }, "node_modules/less/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -9943,6 +10204,8 @@ }, "node_modules/levn": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "license": "MIT", "dependencies": { @@ -9955,6 +10218,8 @@ }, "node_modules/license-webpack-plugin": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, "license": "ISC", "dependencies": { @@ -9971,6 +10236,8 @@ }, "node_modules/lilconfig": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", "dev": true, "license": "MIT", "engines": { @@ -9979,11 +10246,15 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/lint-staged": { "version": "12.5.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.5.0.tgz", + "integrity": "sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g==", "dev": true, "license": "MIT", "dependencies": { @@ -10013,7 +10284,9 @@ } }, "node_modules/lint-staged/node_modules/supports-color": { - "version": "9.2.3", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", "dev": true, "license": "MIT", "engines": { @@ -10025,6 +10298,8 @@ }, "node_modules/listr2": { "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", "dev": true, "license": "MIT", "dependencies": { @@ -10049,22 +10324,10 @@ } } }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/listr2/node_modules/cli-truncate": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "license": "MIT", "dependencies": { @@ -10078,24 +10341,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/listr2/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/listr2/node_modules/slice-ansi": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10109,6 +10358,8 @@ }, "node_modules/loader-runner": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "license": "MIT", "engines": { @@ -10116,7 +10367,9 @@ } }, "node_modules/loader-utils": { - "version": "3.2.0", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, "license": "MIT", "engines": { @@ -10125,6 +10378,8 @@ }, "node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -10135,10 +10390,14 @@ }, "node_modules/lodash": { "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, "node_modules/lodash._baseassign": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10148,16 +10407,22 @@ }, "node_modules/lodash._basecopy": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==", "dev": true, "license": "MIT" }, "node_modules/lodash._bindcallback": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==", "dev": true, "license": "MIT" }, "node_modules/lodash._createassigner": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==", "dev": true, "license": "MIT", "dependencies": { @@ -10168,16 +10433,22 @@ }, "node_modules/lodash._getnative": { "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", "dev": true, "license": "MIT" }, "node_modules/lodash._isiterateecall": { "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.assign": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==", "dev": true, "license": "MIT", "dependencies": { @@ -10188,21 +10459,29 @@ }, "node_modules/lodash.debounce": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true, "license": "MIT" }, "node_modules/lodash.isarguments": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isarray": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.keys": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10213,11 +10492,15 @@ }, "node_modules/lodash.restparam": { "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", "dev": true, "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -10230,66 +10513,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/log-update": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "license": "MIT", "dependencies": { @@ -10305,38 +10532,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-update/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -10349,19 +10548,25 @@ } }, "node_modules/long": { - "version": "5.2.0", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", "license": "Apache-2.0" }, "node_modules/lru-cache": { - "version": "7.14.0", - "devOptional": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", - "engines": { - "node": ">=12" + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/macos-release": { - "version": "2.5.0", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", "dev": true, "license": "MIT", "engines": { @@ -10373,6 +10578,8 @@ }, "node_modules/magic-string": { "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" @@ -10383,6 +10590,8 @@ }, "node_modules/make-dir": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -10396,7 +10605,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -10405,11 +10616,15 @@ }, "node_modules/make-error": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10434,8 +10649,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/marked": { - "version": "4.1.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -10446,6 +10673,8 @@ }, "node_modules/md5.js": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "license": "MIT", "dependencies": { "hash-base": "^3.0.0", @@ -10455,6 +10684,8 @@ }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", "engines": { @@ -10462,28 +10693,39 @@ } }, "node_modules/memfs": { - "version": "3.4.7", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "devOptional": true, "license": "MIT", "engines": { @@ -10492,6 +10734,8 @@ }, "node_modules/methods": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, "license": "MIT", "engines": { @@ -10499,11 +10743,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "devOptional": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -10511,18 +10757,24 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -10531,6 +10783,8 @@ }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -10542,6 +10796,8 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { "node": ">=6" @@ -10549,6 +10805,8 @@ }, "node_modules/mini-css-extract-plugin": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", + "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", "dev": true, "license": "MIT", "dependencies": { @@ -10566,14 +10824,16 @@ } }, "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -10585,11 +10845,15 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true, "license": "ISC" }, "node_modules/minimatch": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10600,12 +10864,19 @@ } }, "node_modules/minimist": { - "version": "1.2.6", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minipass": { - "version": "3.3.4", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10617,6 +10888,8 @@ }, "node_modules/minipass-collect": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10628,6 +10901,8 @@ }, "node_modules/minipass-fetch": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10644,6 +10919,8 @@ }, "node_modules/minipass-flush": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10654,7 +10931,9 @@ } }, "node_modules/minipass-json-stream": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10664,6 +10943,8 @@ }, "node_modules/minipass-pipeline": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10675,6 +10956,8 @@ }, "node_modules/minipass-sized": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "devOptional": true, "license": "ISC", "dependencies": { @@ -10684,8 +10967,17 @@ "node": ">=8" } }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, "node_modules/minizlib": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10696,8 +10988,17 @@ "node": ">= 8" } }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, "node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "devOptional": true, "license": "MIT", "bin": { @@ -10709,10 +11010,14 @@ }, "node_modules/monaco-editor": { "version": "0.33.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.33.0.tgz", + "integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==", "license": "MIT" }, "node_modules/mrmime": { - "version": "1.0.1", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, "license": "MIT", "engines": { @@ -10721,11 +11026,15 @@ }, "node_modules/ms": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "devOptional": true, "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, "license": "MIT", "dependencies": { @@ -10738,6 +11047,8 @@ }, "node_modules/multimatch": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", "license": "MIT", "optional": true, "dependencies": { @@ -10756,6 +11067,8 @@ }, "node_modules/multimatch/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "optional": true, "dependencies": { @@ -10765,6 +11078,8 @@ }, "node_modules/multimatch/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "optional": true, "dependencies": { @@ -10776,6 +11091,8 @@ }, "node_modules/mustache": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", "bin": { "mustache": "bin/mustache" @@ -10783,12 +11100,22 @@ }, "node_modules/mute-stream": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "devOptional": true, "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.4", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" @@ -10798,12 +11125,13 @@ } }, "node_modules/needle": { - "version": "3.1.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -10814,17 +11142,10 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/needle/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "optional": true, @@ -10836,13 +11157,17 @@ } }, "node_modules/needle/node_modules/sax": { - "version": "1.2.4", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "license": "ISC", "optional": true }, "node_modules/negotiator": { - "version": "0.6.3", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "devOptional": true, "license": "MIT", "engines": { @@ -10851,59 +11176,25 @@ }, "node_modules/neo-async": { "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, "node_modules/netmask": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" } }, - "node_modules/ng-morph": { - "version": "2.1.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "jsonc-parser": "3.0.0", - "minimatch": "3.0.4", - "multimatch": "5.0.0", - "ts-morph": "10.0.2" - }, - "peerDependencies": { - "@angular-devkit/core": ">=11.0.0", - "@angular-devkit/schematics": ">=11.0.0" - } - }, - "node_modules/ng-morph/node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/ng-morph/node_modules/jsonc-parser": { - "version": "3.0.0", - "license": "MIT", - "optional": true - }, - "node_modules/ng-morph/node_modules/minimatch": { - "version": "3.0.4", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ng-packagr": { - "version": "14.2.1", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-14.2.2.tgz", + "integrity": "sha512-AqwHcMM6x+JkCHT++IsbulnTdyoXcC2Cr4tbPamuieacc77+fFbB195hdcqEFwsKX5410cymx/ZUyHird9rxlg==", "dev": true, "license": "MIT", "dependencies": { @@ -10949,6 +11240,8 @@ }, "node_modules/ng-qrcode": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ng-qrcode/-/ng-qrcode-7.0.0.tgz", + "integrity": "sha512-Mx7nf8rtGMVYxGe2qfy8/JNiCnxKD7uFsqpP2Hm5eJSQrOEapQl9FR0yuK0I4MMQorJ7s8mZZDxmszQiH8R2Kg==", "license": "MIT", "dependencies": { "qrcode": "^1.5.0", @@ -10961,6 +11254,8 @@ }, "node_modules/nice-napi": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10975,27 +11270,34 @@ }, "node_modules/node-addon-api": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-forge": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-gyp": { - "version": "9.1.0", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", "devOptional": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", - "nopt": "^5.0.0", + "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", @@ -11006,11 +11308,13 @@ "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.22 || ^14.13 || >=16" + "node": "^12.13 || ^14.13 || >=16" } }, "node_modules/node-gyp-build": { - "version": "4.5.0", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true, "license": "MIT", "optional": true, @@ -11022,6 +11326,8 @@ }, "node_modules/node-gyp/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11031,6 +11337,9 @@ }, "node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, "license": "ISC", "dependencies": { @@ -11050,6 +11359,8 @@ }, "node_modules/node-gyp/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11061,6 +11372,8 @@ }, "node_modules/node-html-parser": { "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", "dev": true, "license": "MIT", "dependencies": { @@ -11070,6 +11383,8 @@ }, "node_modules/node-jose": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz", + "integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==", "license": "Apache-2.0", "dependencies": { "base64url": "^3.0.1", @@ -11084,33 +11399,45 @@ } }, "node_modules/node-jose/node_modules/uuid": { - "version": "9.0.0", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/node-releases": { - "version": "2.0.6", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true, "license": "MIT" }, "node_modules/nopt": { - "version": "5.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", "devOptional": true, "license": "ISC", "dependencies": { - "abbrev": "1" + "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/normalize-package-data": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", "devOptional": true, "license": "BSD-2-Clause", "dependencies": { @@ -11125,6 +11452,8 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "devOptional": true, "license": "MIT", "engines": { @@ -11133,6 +11462,8 @@ }, "node_modules/normalize-range": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { @@ -11141,6 +11472,8 @@ }, "node_modules/npm-bundled": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11149,6 +11482,8 @@ }, "node_modules/npm-install-checks": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", + "integrity": "sha512-65lUsMI8ztHCxFz5ckCEC44DRvEGdZX5usQFriauxHEwt7upv1FKaQEmAtU0YnOAdwuNWCmk64xYiQABNrEyLA==", "devOptional": true, "license": "BSD-2-Clause", "dependencies": { @@ -11160,11 +11495,15 @@ }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "devOptional": true, "license": "ISC" }, "node_modules/npm-package-arg": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.0.tgz", + "integrity": "sha512-4J0GL+u2Nh6OnhvUKXRr2ZMG4lR8qtLp+kv7UiV00Y+nGiSxtttCyIRHCt5L5BNkXQld/RceYItau3MDOoGiBw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11179,6 +11518,8 @@ }, "node_modules/npm-packlist": { "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11196,6 +11537,8 @@ }, "node_modules/npm-packlist/node_modules/npm-bundled": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11207,6 +11550,8 @@ }, "node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", "devOptional": true, "license": "ISC", "engines": { @@ -11215,6 +11560,8 @@ }, "node_modules/npm-pick-manifest": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-7.0.1.tgz", + "integrity": "sha512-IA8+tuv8KujbsbLQvselW2XQgmXWS47t3CB0ZrzsRZ82DbDfkcFunOaPm4X7qNuhMfq+FmV7hQT4iFVpHqV7mg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11229,6 +11576,8 @@ }, "node_modules/npm-registry-fetch": { "version": "13.3.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz", + "integrity": "sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11246,6 +11595,8 @@ }, "node_modules/npm-run-path": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { @@ -11257,6 +11608,9 @@ }, "node_modules/npmlog": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "devOptional": true, "license": "ISC", "dependencies": { @@ -11271,6 +11625,8 @@ }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -11281,31 +11637,11 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, "engines": { "node": ">= 0.4" }, @@ -11315,11 +11651,15 @@ }, "node_modules/obuf": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true, "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -11331,6 +11671,8 @@ }, "node_modules/on-headers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, "license": "MIT", "engines": { @@ -11339,6 +11681,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11347,6 +11691,8 @@ }, "node_modules/onetime": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11360,6 +11706,8 @@ }, "node_modules/open": { "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11376,6 +11724,8 @@ }, "node_modules/opencollective-postinstall": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true, "license": "MIT", "bin": { @@ -11384,6 +11734,8 @@ }, "node_modules/opener": { "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, "license": "(WTFPL OR MIT)", "bin": { @@ -11392,6 +11744,8 @@ }, "node_modules/optionator": { "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "license": "MIT", "dependencies": { @@ -11408,6 +11762,8 @@ }, "node_modules/ora": { "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -11427,66 +11783,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/os-name": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", + "integrity": "sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==", "dev": true, "license": "MIT", "dependencies": { @@ -11502,6 +11802,8 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "devOptional": true, "license": "MIT", "engines": { @@ -11510,6 +11812,8 @@ }, "node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11523,6 +11827,8 @@ }, "node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -11533,6 +11839,8 @@ }, "node_modules/p-map": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11547,6 +11855,8 @@ }, "node_modules/p-retry": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11559,6 +11869,8 @@ }, "node_modules/p-retry/node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -11567,6 +11879,8 @@ }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -11574,6 +11888,8 @@ }, "node_modules/pac-proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11593,6 +11909,8 @@ }, "node_modules/pac-proxy-agent/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, "license": "MIT", "engines": { @@ -11601,6 +11919,8 @@ }, "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -11614,6 +11934,8 @@ }, "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11627,6 +11949,8 @@ }, "node_modules/pac-resolver": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.1.tgz", + "integrity": "sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11638,13 +11962,10 @@ "node": ">= 8" } }, - "node_modules/pac-resolver/node_modules/ip": { - "version": "1.1.8", - "dev": true, - "license": "MIT" - }, "node_modules/pacote": { "version": "13.6.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.2.tgz", + "integrity": "sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -11678,11 +11999,15 @@ } }, "node_modules/pako": { - "version": "2.0.4", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -11694,6 +12019,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -11711,6 +12038,8 @@ }, "node_modules/parse-node-version": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, "license": "MIT", "engines": { @@ -11719,10 +12048,14 @@ }, "node_modules/parse5": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "license": "MIT" }, "node_modules/parse5-html-rewriting-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", "license": "MIT", "dependencies": { "parse5": "^6.0.1", @@ -11731,6 +12064,8 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "dev": true, "license": "MIT", "dependencies": { @@ -11739,6 +12074,8 @@ }, "node_modules/parse5-sax-parser": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", "license": "MIT", "dependencies": { "parse5": "^6.0.1" @@ -11746,6 +12083,8 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -11758,11 +12097,14 @@ }, "node_modules/path-browserify": { "version": "1.0.1", - "license": "MIT", - "optional": true + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -11770,6 +12112,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "devOptional": true, "license": "MIT", "engines": { @@ -11778,6 +12122,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -11786,16 +12132,22 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "devOptional": true, "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.7", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { @@ -11804,6 +12156,8 @@ }, "node_modules/pbkdf2": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "license": "MIT", "dependencies": { "create-hash": "^1.1.2", @@ -11817,12 +12171,16 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "devOptional": true, "license": "MIT", "engines": { @@ -11834,6 +12192,8 @@ }, "node_modules/pidtree": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz", + "integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==", "dev": true, "license": "MIT", "bin": { @@ -11845,6 +12205,8 @@ }, "node_modules/pify": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { @@ -11853,6 +12215,8 @@ }, "node_modules/piscina": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", "dev": true, "license": "MIT", "dependencies": { @@ -11866,6 +12230,8 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11877,6 +12243,8 @@ }, "node_modules/please-upgrade-node": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", "dev": true, "license": "MIT", "dependencies": { @@ -11885,13 +12253,17 @@ }, "node_modules/pngjs": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/postcss": { - "version": "8.4.16", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -11901,11 +12273,15 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -11915,6 +12291,8 @@ }, "node_modules/postcss-attribute-case-insensitive": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11931,8 +12309,24 @@ "postcss": "^8.2" } }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-clamp": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -11947,6 +12341,8 @@ }, "node_modules/postcss-color-functional-notation": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -11965,6 +12361,8 @@ }, "node_modules/postcss-color-hex-alpha": { "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11983,6 +12381,8 @@ }, "node_modules/postcss-color-rebeccapurple": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12001,6 +12401,8 @@ }, "node_modules/postcss-custom-media": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", "dev": true, "license": "MIT", "dependencies": { @@ -12018,7 +12420,9 @@ } }, "node_modules/postcss-custom-properties": { - "version": "12.1.9", + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12037,6 +12441,8 @@ }, "node_modules/postcss-custom-selectors": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", "dev": true, "license": "MIT", "dependencies": { @@ -12053,8 +12459,24 @@ "postcss": "^8.3" } }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-dir-pseudo-class": { "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12071,8 +12493,24 @@ "postcss": "^8.2" } }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-double-position-gradients": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12092,6 +12530,8 @@ }, "node_modules/postcss-env-function": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12106,6 +12546,8 @@ }, "node_modules/postcss-focus-visible": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12118,8 +12560,24 @@ "postcss": "^8.4" } }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-focus-within": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12132,8 +12590,24 @@ "postcss": "^8.4" } }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-font-variant": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12142,6 +12616,8 @@ }, "node_modules/postcss-gap-properties": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", "dev": true, "license": "CC0-1.0", "engines": { @@ -12157,6 +12633,8 @@ }, "node_modules/postcss-image-set-function": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12175,6 +12653,8 @@ }, "node_modules/postcss-import": { "version": "15.0.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.0.0.tgz", + "integrity": "sha512-Y20shPQ07RitgBGv2zvkEAu9bqvrD77C9axhj/aA1BQj4czape2MdClCExvB27EwYEJdGgKZBpKanb0t1rK2Kg==", "dev": true, "license": "MIT", "dependencies": { @@ -12191,6 +12671,8 @@ }, "node_modules/postcss-initial": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12199,6 +12681,8 @@ }, "node_modules/postcss-lab-function": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12218,6 +12702,8 @@ }, "node_modules/postcss-loader": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.1.tgz", + "integrity": "sha512-VRviFEyYlLjctSM93gAZtcJJ/iSkPZ79zWbN/1fSH+NisBByEiVLqpdVDrPLVSi8DX0oJo12kL/GppTBdKVXiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12239,6 +12725,8 @@ }, "node_modules/postcss-logical": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", "dev": true, "license": "CC0-1.0", "engines": { @@ -12250,6 +12738,8 @@ }, "node_modules/postcss-media-minmax": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", "dev": true, "license": "MIT", "engines": { @@ -12260,7 +12750,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "license": "ISC", "engines": { @@ -12271,12 +12763,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", + "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -12287,11 +12781,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -12302,6 +12798,8 @@ }, "node_modules/postcss-modules-values": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, "license": "ISC", "dependencies": { @@ -12314,27 +12812,62 @@ "postcss": "^8.1.0" } }, - "node_modules/postcss-nesting": { - "version": "10.2.0", + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "dev": true, "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, "engines": { - "node": "^12 || ^14 || >=16" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.2" + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, "node_modules/postcss-opacity-percentage": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", "dev": true, "funding": [ { @@ -12349,10 +12882,15 @@ "license": "MIT", "engines": { "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" } }, "node_modules/postcss-overflow-shorthand": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12371,6 +12909,8 @@ }, "node_modules/postcss-page-break": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12379,6 +12919,8 @@ }, "node_modules/postcss-place": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12397,6 +12939,8 @@ }, "node_modules/postcss-preset-env": { "version": "7.8.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.0.tgz", + "integrity": "sha512-leqiqLOellpLKfbHkD06E04P6d9ZQ24mat6hu4NSqun7WG0UhspHR5Myiv/510qouCjoo4+YJtNOqg5xHaFnCA==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12463,6 +13007,8 @@ }, "node_modules/postcss-pseudo-class-any-link": { "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -12479,8 +13025,24 @@ "postcss": "^8.2" } }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-replace-overflow-wrap": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12489,6 +13051,8 @@ }, "node_modules/postcss-selector-not": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12505,8 +13069,24 @@ "postcss": "^8.2" } }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12519,6 +13099,8 @@ }, "node_modules/postcss-url": { "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", + "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", "dev": true, "license": "MIT", "dependencies": { @@ -12536,6 +13118,8 @@ }, "node_modules/postcss-url/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -12545,6 +13129,8 @@ }, "node_modules/postcss-url/node_modules/mime": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", "dev": true, "license": "MIT", "bin": { @@ -12556,6 +13142,8 @@ }, "node_modules/postcss-url/node_modules/minimatch": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, "license": "ISC", "dependencies": { @@ -12567,18 +13155,24 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", "bin": { @@ -12593,6 +13187,8 @@ }, "node_modules/pretty-bytes": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, "license": "MIT", "engines": { @@ -12604,6 +13200,8 @@ }, "node_modules/proc-log": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", "devOptional": true, "license": "ISC", "engines": { @@ -12612,6 +13210,8 @@ }, "node_modules/process": { "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -12619,16 +13219,22 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "license": "MIT" }, "node_modules/promise-inflight": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "devOptional": true, "license": "ISC" }, "node_modules/promise-retry": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -12641,6 +13247,8 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -12653,6 +13261,8 @@ }, "node_modules/proxy-addr/node_modules/ipaddr.js": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -12661,6 +13271,8 @@ }, "node_modules/proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", "dev": true, "license": "MIT", "dependencies": { @@ -12679,6 +13291,8 @@ }, "node_modules/proxy-agent/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, "license": "MIT", "engines": { @@ -12687,6 +13301,8 @@ }, "node_modules/proxy-agent/node_modules/http-proxy-agent": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -12698,16 +13314,10 @@ "node": ">= 6" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/proxy-agent/node_modules/socks-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12719,24 +13329,25 @@ "node": ">= 6" } }, - "node_modules/proxy-agent/node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, "node_modules/proxy-from-env": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true, "license": "MIT" }, "node_modules/prr": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/pump": { - "version": "3.0.0", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "license": "MIT", "dependencies": { @@ -12745,18 +13356,21 @@ } }, "node_modules/punycode": { - "version": "2.1.1", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qrcode": { - "version": "1.5.1", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, @@ -12767,21 +13381,10 @@ "node": ">=10.13.0" } }, - "node_modules/qrcode/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -12789,22 +13392,10 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/qrcode/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/qrcode/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/qrcode/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12817,10 +13408,14 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -12841,6 +13436,8 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -12851,11 +13448,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12866,6 +13465,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "devOptional": true, "funding": [ { @@ -12885,6 +13486,8 @@ }, "node_modules/randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12893,6 +13496,8 @@ }, "node_modules/range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { @@ -12900,7 +13505,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "license": "MIT", "dependencies": { @@ -12915,6 +13522,8 @@ }, "node_modules/raw-loader": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", "dev": true, "license": "MIT", "dependencies": { @@ -12934,6 +13543,8 @@ }, "node_modules/raw-loader/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -12949,6 +13560,8 @@ }, "node_modules/raw-loader/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12957,11 +13570,15 @@ }, "node_modules/raw-loader/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/raw-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -12974,7 +13591,9 @@ } }, "node_modules/raw-loader/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", "dependencies": { @@ -12992,6 +13611,8 @@ }, "node_modules/read-cache": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, "license": "MIT", "dependencies": { @@ -13000,6 +13621,9 @@ }, "node_modules/read-package-json": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.2.tgz", + "integrity": "sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "devOptional": true, "license": "ISC", "dependencies": { @@ -13014,6 +13638,8 @@ }, "node_modules/read-package-json-fast": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", + "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -13026,6 +13652,8 @@ }, "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", "devOptional": true, "license": "ISC", "engines": { @@ -13033,7 +13661,9 @@ } }, "node_modules/readable-stream": { - "version": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -13046,6 +13676,8 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13056,17 +13688,23 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true, "license": "Apache-2.0" }, "node_modules/regenerate": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "license": "MIT", "dependencies": { @@ -13078,11 +13716,15 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { - "version": "0.15.0", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, "license": "MIT", "dependencies": { @@ -13090,51 +13732,67 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true, "license": "MIT" }, "node_modules/regexpu-core": { - "version": "5.2.1", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" + "unicode-match-property-value-ecmascript": "^2.1.0" }, "engines": { "node": ">=4" } }, "node_modules/regjsgen": { - "version": "0.7.1", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "dev": true, "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.9.1", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13142,6 +13800,8 @@ }, "node_modules/require-from-string": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13149,15 +13809,21 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, "node_modules/requires-port": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13174,6 +13840,8 @@ }, "node_modules/resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { @@ -13182,6 +13850,8 @@ }, "node_modules/resolve-url-loader": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -13197,6 +13867,8 @@ }, "node_modules/resolve-url-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -13210,6 +13882,8 @@ }, "node_modules/resolve-url-loader/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13218,6 +13892,8 @@ }, "node_modules/restore-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -13229,6 +13905,8 @@ }, "node_modules/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "devOptional": true, "license": "MIT", "engines": { @@ -13237,6 +13915,8 @@ }, "node_modules/reusify": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "devOptional": true, "license": "MIT", "engines": { @@ -13245,12 +13925,17 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, "license": "MIT" }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "devOptional": true, "license": "ISC", "dependencies": { @@ -13265,6 +13950,8 @@ }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13274,6 +13961,9 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, "license": "ISC", "dependencies": { @@ -13293,6 +13983,8 @@ }, "node_modules/rimraf/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, "license": "ISC", "dependencies": { @@ -13304,6 +13996,8 @@ }, "node_modules/ripemd160": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "license": "MIT", "dependencies": { "hash-base": "^3.0.0", @@ -13311,7 +14005,9 @@ } }, "node_modules/rollup": { - "version": "2.79.0", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", "bin": { @@ -13326,6 +14022,8 @@ }, "node_modules/rollup-plugin-sourcemaps": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz", + "integrity": "sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==", "dev": true, "license": "MIT", "dependencies": { @@ -13347,6 +14045,8 @@ }, "node_modules/rsvp": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", "dev": true, "license": "MIT", "engines": { @@ -13355,6 +14055,8 @@ }, "node_modules/run-async": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "devOptional": true, "license": "MIT", "engines": { @@ -13363,6 +14065,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "devOptional": true, "funding": [ { @@ -13387,21 +14091,42 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/safe-buffer": { - "version": "5.1.2", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "devOptional": true, "license": "MIT" }, "node_modules/sass": { "version": "1.54.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", + "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", "dev": true, "license": "MIT", "dependencies": { @@ -13418,6 +14143,8 @@ }, "node_modules/sass-loader": { "version": "13.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.0.2.tgz", + "integrity": "sha512-BbiqbVmbfJaWVeOOAu2o7DhYWtcNmTfvroVgFXa6k2hHheMxNAeDHLNoDy/Q5aoaVlz0LH+MbMktKwm9vN/j8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13455,11 +14182,15 @@ }, "node_modules/sax": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", "dev": true, "license": "ISC" }, "node_modules/schema-utils": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -13477,6 +14208,8 @@ }, "node_modules/schema-utils/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -13492,6 +14225,8 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -13500,19 +14235,26 @@ }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/select-hose": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true, "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.1.1", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -13520,7 +14262,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -13535,11 +14279,15 @@ }, "node_modules/semver-compare": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT" }, "node_modules/semver-regex": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", + "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", "dev": true, "license": "MIT", "engines": { @@ -13551,6 +14299,8 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "devOptional": true, "license": "ISC", "dependencies": { @@ -13560,8 +14310,17 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, "node_modules/send": { - "version": "0.18.0", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -13585,6 +14344,8 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -13593,24 +14354,45 @@ }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/send/node_modules/depd": { - "version": "2.0.0", + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "6.0.0", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13619,6 +14401,8 @@ }, "node_modules/serve-index": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, "license": "MIT", "dependencies": { @@ -13636,14 +14420,28 @@ }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, "license": "MIT", "dependencies": { @@ -13658,21 +14456,29 @@ }, "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { @@ -13680,14 +14486,16 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -13695,15 +14503,39 @@ }, "node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/sha.js": { "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", @@ -13715,6 +14547,8 @@ }, "node_modules/shallow-clone": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "license": "MIT", "dependencies": { @@ -13726,6 +14560,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -13737,6 +14573,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -13744,13 +14582,19 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13758,16 +14602,20 @@ }, "node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/sirv": { - "version": "1.0.19", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, "engines": { "node": ">= 10" @@ -13775,6 +14623,8 @@ }, "node_modules/slash": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, "license": "MIT", "engines": { @@ -13786,6 +14636,8 @@ }, "node_modules/slice-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13800,38 +14652,10 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "devOptional": true, "license": "MIT", "engines": { @@ -13841,6 +14665,8 @@ }, "node_modules/sockjs": { "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13850,20 +14676,24 @@ } }, "node_modules/socks": { - "version": "2.7.0", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "devOptional": true, "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13877,13 +14707,17 @@ }, "node_modules/source-map": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13892,6 +14726,8 @@ }, "node_modules/source-map-loader": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.0.tgz", + "integrity": "sha512-i3KVgM3+QPAHNbGavK+VBq03YoJl24m9JWNbLgsjTj8aJzXG9M61bantBTNBt7CNwY2FYf+RJRYJ3pzalKjIrw==", "dev": true, "license": "MIT", "dependencies": { @@ -13912,6 +14748,8 @@ }, "node_modules/source-map-loader/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -13923,6 +14761,9 @@ }, "node_modules/source-map-resolve": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "license": "MIT", "dependencies": { @@ -13932,6 +14773,8 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -13941,6 +14784,8 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13949,10 +14794,15 @@ }, "node_modules/sourcemap-codec": { "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, "node_modules/spdx-correct": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -13961,12 +14811,16 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "devOptional": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13975,12 +14829,16 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "devOptional": true, "license": "CC0-1.0" }, "node_modules/spdy": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, "license": "MIT", "dependencies": { @@ -13996,6 +14854,8 @@ }, "node_modules/spdy-transport": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "license": "MIT", "dependencies": { @@ -14009,6 +14869,8 @@ }, "node_modules/split2": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "dev": true, "license": "ISC", "dependencies": { @@ -14016,21 +14878,29 @@ } }, "node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/ssh-config": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ssh-config/-/ssh-config-1.1.6.tgz", + "integrity": "sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==", "dev": true, "license": "MIT" }, "node_modules/ssr-window": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", + "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==", "license": "MIT" }, "node_modules/ssri": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "devOptional": true, "license": "ISC", "dependencies": { @@ -14042,6 +14912,8 @@ }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, "license": "MIT", "engines": { @@ -14050,6 +14922,8 @@ }, "node_modules/stream-combiner2": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", "dev": true, "license": "MIT", "dependencies": { @@ -14058,7 +14932,9 @@ } }, "node_modules/stream-combiner2/node_modules/readable-stream": { - "version": "2.3.7", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -14071,8 +14947,17 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/stream-combiner2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-combiner2/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { @@ -14081,31 +14966,17 @@ }, "node_modules/string_decoder": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/string-argv": { - "version": "0.3.1", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", "engines": { @@ -14114,6 +14985,8 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14126,6 +14999,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14136,6 +15011,8 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "license": "MIT", "engines": { @@ -14144,6 +15021,8 @@ }, "node_modules/stylus": { "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, "license": "MIT", "dependencies": { @@ -14165,6 +15044,8 @@ }, "node_modules/stylus-loader": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-7.0.0.tgz", + "integrity": "sha512-WTbtLrNfOfLgzTaR9Lj/BPhQroKk/LC1hfTXSUbrxmxgfUo3Y3LpmKRVA2R1XbjvTAvOfaian9vOyfv1z99E+A==", "dev": true, "license": "MIT", "dependencies": { @@ -14186,6 +15067,8 @@ }, "node_modules/stylus/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -14195,6 +15078,9 @@ }, "node_modules/stylus/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -14214,6 +15100,8 @@ }, "node_modules/stylus/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -14225,11 +15113,16 @@ }, "node_modules/stylus/node_modules/sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "license": "ISC" }, "node_modules/superagent": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", + "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", + "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", "dev": true, "license": "MIT", "dependencies": { @@ -14251,6 +15144,8 @@ }, "node_modules/superagent-proxy": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-wAlRInOeDFyd9pyonrkJspdRAxdLrcsZ6aSnS+8+nu4x1aXbz6FWSTT9M6Ibze+eG60szlL7JA8wEIV7bPWuyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14266,6 +15161,8 @@ }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -14276,18 +15173,21 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "dev": true, + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "devOptional": true, "license": "MIT", "engines": { @@ -14298,7 +15198,9 @@ } }, "node_modules/swiper": { - "version": "8.4.2", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", + "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", "funding": [ { "type": "patreon", @@ -14321,6 +15223,8 @@ }, "node_modules/symbol-observable": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "devOptional": true, "license": "MIT", "engines": { @@ -14329,6 +15233,8 @@ }, "node_modules/tapable": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, "license": "MIT", "engines": { @@ -14336,23 +15242,44 @@ } }, "node_modules/tar": { - "version": "6.1.11", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "devOptional": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=8" } }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, "node_modules/terser": { "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -14369,15 +15296,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.6", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -14403,6 +15332,8 @@ }, "node_modules/terser-webpack-plugin/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -14418,19 +15349,32 @@ }, "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", "dependencies": { @@ -14446,13 +15390,36 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", "dependencies": { @@ -14466,6 +15433,8 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -14475,6 +15444,9 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -14494,6 +15466,8 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -14505,25 +15479,35 @@ }, "node_modules/text-mask-core": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/text-mask-core/-/text-mask-core-5.1.2.tgz", + "integrity": "sha512-VfkCMdmRRZqXgQZFlDMiavm3hzsMzBM23CxHZsaeAYg66ZhXCNJWrFmnJwNy8KF9f74YvAUAuQenxsMCfuvhUw==", "license": "Unlicense" }, "node_modules/text-table": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/through": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "devOptional": true, "license": "MIT" }, "node_modules/thunky": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true, "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -14533,16 +15517,10 @@ "node": ">=0.6.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -14554,6 +15532,8 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -14561,7 +15541,9 @@ } }, "node_modules/totalist": { - "version": "1.1.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -14570,6 +15552,8 @@ }, "node_modules/tree-kill": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -14577,20 +15561,26 @@ } }, "node_modules/ts-matches": { - "version": "v5.2.1", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.2.1.tgz", + "integrity": "sha512-qdnMgTHsGCEGGK6QiaNMY2vD9eQtRp2Q+pAxcOAzxHJKDKTBYsc1ISTg1zp8H2+EmtCB0eko/1TwYUA5/mUGug==", "license": "MIT" }, "node_modules/ts-morph": { - "version": "10.0.2", + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", + "integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==", "license": "MIT", "optional": true, "dependencies": { - "@ts-morph/common": "~0.9.0", - "code-block-writer": "^10.1.1" + "@ts-morph/common": "~0.24.0", + "code-block-writer": "^13.0.1" } }, "node_modules/ts-node": { - "version": "10.9.1", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14632,11 +15622,16 @@ } }, "node_modules/tslib": { - "version": "2.4.0", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tslint": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -14664,8 +15659,23 @@ "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" } }, + "node_modules/tslint/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/tslint/node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { @@ -14674,6 +15684,8 @@ }, "node_modules/tslint/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -14683,19 +15695,58 @@ }, "node_modules/tslint/node_modules/builtin-modules": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/tslint/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/tslint/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslint/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/tslint/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -14713,8 +15764,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tslint/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/tslint/node_modules/js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", "dependencies": { @@ -14727,6 +15790,8 @@ }, "node_modules/tslint/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -14738,6 +15803,8 @@ }, "node_modules/tslint/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { @@ -14748,20 +15815,46 @@ } }, "node_modules/tslint/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" } }, + "node_modules/tslint/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslint/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/tslint/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/tsutils": { "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "license": "MIT", "dependencies": { @@ -14773,11 +15866,15 @@ }, "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/type-check": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, "license": "MIT", "dependencies": { @@ -14789,6 +15886,8 @@ }, "node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "devOptional": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -14800,6 +15899,8 @@ }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", "dependencies": { @@ -14812,11 +15913,15 @@ }, "node_modules/typed-assert": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", "dev": true, "license": "MIT" }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14825,6 +15930,8 @@ }, "node_modules/typescript": { "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -14836,7 +15943,9 @@ } }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "license": "MIT", "engines": { @@ -14845,6 +15954,8 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14856,7 +15967,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.0.0", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "license": "MIT", "engines": { @@ -14865,6 +15978,8 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, "license": "MIT", "engines": { @@ -14873,6 +15988,8 @@ }, "node_modules/unique-filename": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -14881,6 +15998,8 @@ }, "node_modules/unique-slug": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "devOptional": true, "license": "ISC", "dependencies": { @@ -14888,7 +16007,9 @@ } }, "node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -14897,6 +16018,8 @@ }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { @@ -14905,6 +16028,8 @@ }, "node_modules/untildify": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, "license": "MIT", "engines": { @@ -14912,7 +16037,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.9", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -14922,15 +16049,19 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -14938,6 +16069,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -14945,10 +16078,14 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", "engines": { @@ -14957,6 +16094,8 @@ }, "node_modules/uuid": { "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -14964,11 +16103,15 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -14978,6 +16121,8 @@ }, "node_modules/validate-npm-package-name": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", + "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", "devOptional": true, "license": "ISC", "dependencies": { @@ -14989,6 +16134,8 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { @@ -14996,7 +16143,10 @@ } }, "node_modules/vm2": { - "version": "3.9.11", + "version": "3.9.19", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", + "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", "dev": true, "license": "MIT", "dependencies": { @@ -15011,7 +16161,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -15024,6 +16176,8 @@ }, "node_modules/wbuf": { "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, "license": "MIT", "dependencies": { @@ -15032,39 +16186,43 @@ }, "node_modules/wcwidth": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "license": "MIT", "dependencies": { "defaults": "^1.0.3" } }, "node_modules/webpack": { - "version": "5.74.0", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -15084,19 +16242,23 @@ } }, "node_modules/webpack-bundle-analyzer": { - "version": "4.8.0", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", "dev": true, "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", - "lodash": "^4.17.20", + "html-escaper": "^2.0.2", "opener": "^1.5.2", - "sirv": "^1.0.7", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { @@ -15106,100 +16268,33 @@ "node": ">= 10.13.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-bundle-analyzer/node_modules/commander": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" } }, - "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "node": ">=10" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-middleware": { "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", "dev": true, "license": "MIT", "dependencies": { @@ -15221,14 +16316,16 @@ } }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -15240,6 +16337,8 @@ }, "node_modules/webpack-dev-server": { "version": "4.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.0.tgz", + "integrity": "sha512-L5S4Q2zT57SK7tazgzjMiSMBdsw+rGYIX27MgPgx7LDhWO0lViPrHKoLS7jo5In06PWYAhlYu3PbyoC6yAThbw==", "dev": true, "license": "MIT", "dependencies": { @@ -15293,14 +16392,16 @@ } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -15310,8 +16411,32 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-merge": { "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15324,6 +16449,8 @@ }, "node_modules/webpack-sources": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, "license": "MIT", "engines": { @@ -15332,6 +16459,8 @@ }, "node_modules/webpack-subresource-integrity": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15350,10 +16479,21 @@ } } }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15367,21 +16507,30 @@ }, "node_modules/webpack/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -15397,6 +16546,8 @@ }, "node_modules/websocket-driver": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -15410,6 +16561,8 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -15418,6 +16571,8 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "devOptional": true, "license": "ISC", "dependencies": { @@ -15431,11 +16586,15 @@ } }, "node_modules/which-module": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, "node_modules/which-pm-runs": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", "dev": true, "license": "MIT", "engines": { @@ -15444,6 +16603,8 @@ }, "node_modules/wide-align": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "devOptional": true, "license": "ISC", "dependencies": { @@ -15451,12 +16612,16 @@ } }, "node_modules/wildcard": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true, "license": "MIT" }, "node_modules/windows-release": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", "dev": true, "license": "MIT", "dependencies": { @@ -15471,6 +16636,8 @@ }, "node_modules/windows-release/node_modules/execa": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "license": "MIT", "dependencies": { @@ -15493,6 +16660,8 @@ }, "node_modules/windows-release/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -15507,6 +16676,8 @@ }, "node_modules/windows-release/node_modules/human-signals": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -15514,7 +16685,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -15523,6 +16696,8 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -15537,43 +16712,17 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "devOptional": true, - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "devOptional": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "license": "ISC", "dependencies": { @@ -15584,11 +16733,13 @@ } }, "node_modules/ws": { - "version": "8.8.1", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=8.3.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -15605,11 +16756,18 @@ }, "node_modules/xregexp": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "*" + } }, "node_modules/xxhashjs": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", "dev": true, "license": "MIT", "dependencies": { @@ -15618,6 +16776,8 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "devOptional": true, "license": "ISC", "engines": { @@ -15625,12 +16785,16 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "devOptional": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -15639,6 +16803,8 @@ }, "node_modules/yargs": { "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -15656,6 +16822,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "devOptional": true, "license": "ISC", "engines": { @@ -15664,6 +16832,8 @@ }, "node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { @@ -15672,6 +16842,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -15683,6 +16855,8 @@ }, "node_modules/zone.js": { "version": "0.11.8", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz", + "integrity": "sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" diff --git a/web/package.json b/web/package.json index 7784543fe..3254fcd17 100644 --- a/web/package.json +++ b/web/package.json @@ -1,19 +1,18 @@ { "name": "startos-ui", - "version": "0.3.5.1", + "version": "0.3.6-alpha.13", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", + "license": "MIT", "scripts": { "ng": "ng", - "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui", + "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup", "check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck", "check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck", - "check:dui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", "check:install-wiz": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", - "build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build", - "build:dui": "ng run diagnostic-ui:build", + "build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)", "build:install-wiz": "ng run install-wizard:build", "build:setup": "ng run setup-wizard:build", "build:ui": "ng run ui:build", @@ -25,7 +24,6 @@ "analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json", "publish:shared": "npm run build:shared && npm publish ./dist/shared --access public", "publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public", - "start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0", "start:install-wiz": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0", "start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0", "start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0", @@ -45,19 +43,27 @@ "@angular/service-worker": "^14.2.2", "@ionic/angular": "^6.1.15", "@materia-ui/ngx-monaco-editor": "^6.0.0", - "@ng-web-apis/common": "^2.0.0", - "@ng-web-apis/mutation-observer": "^2.0.0", - "@ng-web-apis/resize-observer": "^2.0.0", + "@ng-web-apis/common": "^3.2.3", + "@ng-web-apis/mutation-observer": "^3.2.3", + "@ng-web-apis/resize-observer": "^3.2.3", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.20.0", - "@taiga-ui/cdk": "3.20.0", - "@taiga-ui/core": "3.20.0", - "@taiga-ui/icons": "3.20.0", - "@taiga-ui/kit": "3.20.0", + "@start9labs/start-sdk": "file:../sdk/baseDist", + "@taiga-ui/addon-charts": "3.96.0", + "@taiga-ui/addon-commerce": "3.96.0", + "@taiga-ui/cdk": "3.96.0", + "@taiga-ui/core": "3.96.0", + "@taiga-ui/experimental": "3.96.0", + "@taiga-ui/icons": "3.96.0", + "@taiga-ui/kit": "3.96.0", + "@tinkoff/ng-dompurify": "4.0.0", + "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "cbor": "npm:@jprochazk/cbor@^0.4.9", "cbor-web": "^8.1.0", "core-js": "^3.21.1", @@ -68,15 +74,17 @@ "jose": "^4.9.0", "js-yaml": "^4.1.0", "marked": "^4.0.0", + "mime": "^4.0.3", "monaco-editor": "^0.33.0", "mustache": "^4.2.0", "ng-qrcode": "^7.0.0", "node-jose": "^2.2.0", - "patch-db-client": "file: ../../../patch-db/client", + "patch-db-client": "file:../patch-db/client", + "path-browserify": "^1.0.1", "pbkdf2": "^3.1.2", "rxjs": "^7.8.1", "swiper": "^8.2.4", - "ts-matches": "^5.2.1", + "ts-matches": "^6.1.0", "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 0a678d4e8..13d3450b2 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,20 +1,22 @@ { "name": null, - "ack-welcome": "0.3.5.1", "marketplace": { - "selected-url": "https://registry.start9.com/", - "known-hosts": { - "https://registry.start9.com/": {}, - "https://community-registry.start9.com/": {} + "selectedUrl": "https://registry.start9.com/", + "knownHosts": { + "https://registry.start9.com/": { + "name": "Start9 Registry" + }, + "https://community-registry.start9.com/": { + "name": "Community Registry" + } } }, - "dev": {}, "gaming": { "snake": { - "high-score": 0 + "highScore": 0 } }, - "ack-instructions": {}, + "ackInstructions": {}, "theme": "Dark", "widgets": [] } diff --git a/web/projects/diagnostic-ui/src/app/app-routing.module.ts b/web/projects/diagnostic-ui/src/app/app-routing.module.ts deleted file mode 100644 index f9f009b48..000000000 --- a/web/projects/diagnostic-ui/src/app/app-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core' -import { PreloadAllModules, RouterModule, Routes } from '@angular/router' - -const routes: Routes = [ - { - path: '', - loadChildren: () => - import('./pages/home/home.module').then(m => m.HomePageModule), - }, - { - path: 'logs', - loadChildren: () => - import('./pages/logs/logs.module').then(m => m.LogsPageModule), - }, -] - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - scrollPositionRestoration: 'enabled', - preloadingStrategy: PreloadAllModules, - useHash: true, - }), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/web/projects/diagnostic-ui/src/app/app.component.html b/web/projects/diagnostic-ui/src/app/app.component.html deleted file mode 100644 index cd28a7e80..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/projects/diagnostic-ui/src/app/app.component.scss b/web/projects/diagnostic-ui/src/app/app.component.scss deleted file mode 100644 index b528fd9bd..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: block; - height: 100%; -} - -tui-root { - height: 100%; -} diff --git a/web/projects/diagnostic-ui/src/app/app.component.ts b/web/projects/diagnostic-ui/src/app/app.component.ts deleted file mode 100644 index 5ac82a652..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], -}) -export class AppComponent { - constructor() {} -} diff --git a/web/projects/diagnostic-ui/src/app/app.module.ts b/web/projects/diagnostic-ui/src/app/app.module.ts deleted file mode 100644 index 1abde53a3..000000000 --- a/web/projects/diagnostic-ui/src/app/app.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NgModule } from '@angular/core' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RouteReuseStrategy } from '@angular/router' -import { IonicModule, IonicRouteStrategy } from '@ionic/angular' -import { TuiRootModule } from '@taiga-ui/core' -import { AppComponent } from './app.component' -import { AppRoutingModule } from './app-routing.module' -import { HttpClientModule } from '@angular/common/http' -import { ApiService } from './services/api/api.service' -import { MockApiService } from './services/api/mock-api.service' -import { LiveApiService } from './services/api/live-api.service' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' - -const { - useMocks, - ui: { api }, -} = require('../../../../config.json') as WorkspaceConfig - -@NgModule({ - declarations: [AppComponent], - imports: [ - HttpClientModule, - BrowserAnimationsModule, - IonicModule.forRoot({ - mode: 'md', - }), - AppRoutingModule, - TuiRootModule, - ], - providers: [ - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - { - provide: ApiService, - useClass: useMocks ? MockApiService : LiveApiService, - }, - { - provide: RELATIVE_URL, - useValue: `/${api.url}/${api.version}`, - }, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts b/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts deleted file mode 100644 index efb1977dc..000000000 --- a/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { HomePage } from './home.page' - -const routes: Routes = [ - { - path: '', - component: HomePage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class HomePageRoutingModule {} diff --git a/web/projects/diagnostic-ui/src/app/services/api/api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/api.service.ts deleted file mode 100644 index 562d486c3..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/api.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LogsRes, ServerLogsReq } from '@start9labs/shared' - -export abstract class ApiService { - abstract getError(): Promise - abstract restart(): Promise - abstract forgetDrive(): Promise - abstract repairDisk(): Promise - abstract systemRebuild(): Promise - abstract getLogs(params: ServerLogsReq): Promise -} - -export interface GetErrorRes { - code: number - message: string - data: { details: string } -} diff --git a/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts deleted file mode 100644 index bbde6e5ba..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Injectable } from '@angular/core' -import { - HttpService, - isRpcError, - RpcError, - RPCOptions, -} from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' -import { LogsRes, ServerLogsReq } from '@start9labs/shared' - -@Injectable() -export class LiveApiService implements ApiService { - constructor(private readonly http: HttpService) {} - - async getError(): Promise { - return this.rpcRequest({ - method: 'diagnostic.error', - params: {}, - }) - } - - async restart(): Promise { - return this.rpcRequest({ - method: 'diagnostic.restart', - params: {}, - }) - } - - async forgetDrive(): Promise { - return this.rpcRequest({ - method: 'diagnostic.disk.forget', - params: {}, - }) - } - - async repairDisk(): Promise { - return this.rpcRequest({ - method: 'diagnostic.disk.repair', - params: {}, - }) - } - - async systemRebuild(): Promise { - return this.rpcRequest({ - method: 'diagnostic.rebuild', - params: {}, - }) - } - - async getLogs(params: ServerLogsReq): Promise { - return this.rpcRequest({ - method: 'diagnostic.logs', - params, - }) - } - - private async rpcRequest(opts: RPCOptions): Promise { - const res = await this.http.rpcRequest(opts) - - const rpcRes = res.body - - if (isRpcError(rpcRes)) { - throw new RpcError(rpcRes.error) - } - - return rpcRes.result - } -} diff --git a/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts deleted file mode 100644 index 5d8c13a4f..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable } from '@angular/core' -import { pauseFor } from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' -import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared' - -@Injectable() -export class MockApiService implements ApiService { - async getError(): Promise { - await pauseFor(1000) - return { - code: 15, - message: 'Unknown server', - data: { details: 'Some details about the error here' }, - } - } - - async restart(): Promise { - await pauseFor(1000) - } - - async forgetDrive(): Promise { - await pauseFor(1000) - } - - async repairDisk(): Promise { - await pauseFor(1000) - } - - async systemRebuild(): Promise { - await pauseFor(1000) - } - - async getLogs(params: ServerLogsReq): Promise { - await pauseFor(1000) - let entries: Log[] - if (Math.random() < 0.2) { - entries = packageLogs - } else { - const arrLength = params.limit - ? Math.ceil(params.limit / packageLogs.length) - : 10 - entries = new Array(arrLength) - .fill(packageLogs) - .reduce((acc, val) => acc.concat(val), []) - } - return { - entries, - 'start-cursor': 'startCursor', - 'end-cursor': 'endCursor', - } - } -} - -const packageLogs = [ - { - timestamp: '2019-12-26T14:20:30.872Z', - message: '****** START *****', - }, - { - timestamp: '2019-12-26T14:21:30.872Z', - message: 'ServerLogs ServerLogs ServerLogs ServerLogs ServerLogs', - }, - { - timestamp: '2019-12-26T14:22:30.872Z', - message: '****** FINISH *****', - }, -] diff --git a/web/projects/diagnostic-ui/src/environments/environment.prod.ts b/web/projects/diagnostic-ui/src/environments/environment.prod.ts deleted file mode 100644 index 970e25bd7..000000000 --- a/web/projects/diagnostic-ui/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true, -} diff --git a/web/projects/diagnostic-ui/src/environments/environment.ts b/web/projects/diagnostic-ui/src/environments/environment.ts deleted file mode 100644 index 5c68c17ab..000000000 --- a/web/projects/diagnostic-ui/src/environments/environment.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false, -} - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/web/projects/diagnostic-ui/src/index.html b/web/projects/diagnostic-ui/src/index.html deleted file mode 100644 index 1822018f3..000000000 --- a/web/projects/diagnostic-ui/src/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - StartOS Diagnostic UI - - - - - - - - - - - - - - - diff --git a/web/projects/diagnostic-ui/src/main.ts b/web/projects/diagnostic-ui/src/main.ts deleted file mode 100644 index 21499c3cd..000000000 --- a/web/projects/diagnostic-ui/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { enableProdMode } from '@angular/core' -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' -import { AppModule } from './app/app.module' -import { environment } from './environments/environment' - -if (environment.production) { - enableProdMode() -} - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch(err => console.error(err)) diff --git a/web/projects/diagnostic-ui/src/polyfills.ts b/web/projects/diagnostic-ui/src/polyfills.ts deleted file mode 100644 index 4437ced44..000000000 --- a/web/projects/diagnostic-ui/src/polyfills.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/guide/browser-support - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - * because those flags need to be set before `zone.js` being loaded, and webpack - * will put import in the top of bundle, so user need to create a separate file - * in this directory (for example: zone-flags.ts), and put the following flags - * into that file, and then add the following code before importing zone.js. - * import './zone-flags'; - * - * The flags allowed in zone-flags.ts are listed here. - * - * The following flags will work for all browsers. - * - * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - * - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - * - * (window as any).__Zone_enable_cross_context_check = true; - * - */ - -import './zone-flags' - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js/dist/zone' // Included with Angular CLI. - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/web/projects/diagnostic-ui/src/styles.scss b/web/projects/diagnostic-ui/src/styles.scss deleted file mode 100644 index ac0aadb69..000000000 --- a/web/projects/diagnostic-ui/src/styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf'); -} - -/** Ionic CSS Variables overrides **/ -:root { - --ion-font-family: 'Montserrat'; - - --ion-color-primary: #0075e1; - - --ion-color-medium: #989aa2; - --ion-color-medium-rgb: 152,154,162; - --ion-color-medium-contrast: #000000; - --ion-color-medium-contrast-rgb: 0,0,0; - --ion-color-medium-shade: #86888f; - --ion-color-medium-tint: #a2a4ab; - - --ion-color-light: #222428; - --ion-color-light-rgb: 34,36,40; - --ion-color-light-contrast: #ffffff; - --ion-color-light-contrast-rgb: 255,255,255; - --ion-color-light-shade: #1e2023; - --ion-color-light-tint: #383a3e; - - --ion-item-background: #2b2b2b; - --ion-toolbar-background: #2b2b2b; - --ion-card-background: #2b2b2b; - - --ion-background-color: #282828; - --ion-background-color-rgb: 30,30,30; - --ion-text-color: var(--ion-color-dark); - --ion-text-color-rgb: var(--ion-color-dark-rgb); -} - -.loader { - --spinner-color: var(--ion-color-warning) !important; - z-index: 40000 !important; -} diff --git a/web/projects/diagnostic-ui/src/zone-flags.ts b/web/projects/diagnostic-ui/src/zone-flags.ts deleted file mode 100644 index 24ca60fe2..000000000 --- a/web/projects/diagnostic-ui/src/zone-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Prevents Angular change detection from - * running with certain Web Component callbacks - */ -// eslint-disable-next-line no-underscore-dangle -(window as any).__Zone_disable_customElements = true diff --git a/web/projects/diagnostic-ui/tsconfig.json b/web/projects/diagnostic-ui/tsconfig.json deleted file mode 100644 index f642f09b3..000000000 --- a/web/projects/diagnostic-ui/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./" - }, - "files": ["src/main.ts", "src/polyfills.ts"], - "include": ["src/**/*.d.ts"] -} diff --git a/web/projects/install-wizard/src/app/app-routing.module.ts b/web/projects/install-wizard/src/app/app-routing.module.ts index 80901192f..ab9831822 100644 --- a/web/projects/install-wizard/src/app/app-routing.module.ts +++ b/web/projects/install-wizard/src/app/app-routing.module.ts @@ -14,7 +14,6 @@ const routes: Routes = [ RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled', preloadingStrategy: PreloadAllModules, - useHash: true, }), ], exports: [RouterModule], diff --git a/web/projects/install-wizard/src/app/services/api/mock-api.service.ts b/web/projects/install-wizard/src/app/services/api/mock-api.service.ts index 9caf4f88e..6b94f18db 100644 --- a/web/projects/install-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/install-wizard/src/app/services/api/mock-api.service.ts @@ -17,12 +17,15 @@ export class MockApiService implements ApiService { label: null, capacity: 73264762332, used: null, - 'embassy-os': { - version: '0.2.17', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -40,12 +43,15 @@ export class MockApiService implements ApiService { label: null, capacity: 73264762332, used: null, - 'embassy-os': { - version: '0.3.3', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -63,12 +69,15 @@ export class MockApiService implements ApiService { label: null, capacity: 73264762332, used: null, - 'embassy-os': { - version: '0.3.2', - full: true, - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': null, + startOs: { + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: 'guid-guid-guid-guid', }, diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html similarity index 63% rename from web/projects/marketplace/src/pages/release-notes/release-notes.component.html rename to web/projects/marketplace/src/modals/release-notes/release-notes.component.html index 74e34c88f..74661827d 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html @@ -1,6 +1,17 @@ - + + + Past Release Notes + + + + + + + + + -