diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index c51eafc5c..bc0fb626e 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -45,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: @@ -75,6 +75,11 @@ jobs: with: submodules: recursive + - 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 }} @@ -83,7 +88,7 @@ jobs: 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 + 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@v3 @@ -148,6 +153,11 @@ jobs: 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 diff --git a/Makefile b/Makefile index c0492ba55..a714cd389 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ 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-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/install-wizard 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) @@ -16,7 +15,7 @@ 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) +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 web/patchdb-ui-seed.json sdk/dist WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) @@ -24,7 +23,7 @@ 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 container-runtime/rootfs.$(ARCH).squashfs +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) ifeq ($(REMOTE),) @@ -48,7 +47,7 @@ endif .DELETE_ON_ERROR: -.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb 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 all: $(ALL_TARGETS) @@ -90,9 +89,16 @@ clean: format: cd core && cargo +nightly fmt -test: $(CORE_SRC) $(ENVIRONMENT_FILE) - (cd core && cargo build && cargo test) - (cd sdk && make 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/lib/osBindings + cd sdk && make test + +test-container-runtime: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json + cd container-runtime && npm test cli: cd core && ./install-cli.sh @@ -225,7 +231,7 @@ sdk/lib/osBindings: core/startos/bindings core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) rm -rf core/startos/bindings - (cd core/ && cargo test --features=test '::export_bindings_') + ./core/build-ts.sh touch core/startos/bindings sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings @@ -257,9 +263,13 @@ 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 -$(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) - cd core && ARCH=$(ARCH) ./build-startos-bins.sh - touch $(BINS) +core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) web/dist/static web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) + ARCH=$(ARCH) ./core/build-startbox.sh + touch core/target/$(ARCH)-unknown-linux-musl/release/startbox + +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/.package-lock.json: web/package.json sdk/dist npm --prefix web ci diff --git a/build-cargo-dep.sh b/build-cargo-dep.sh index c32e4f8ae..922dfbdf9 100755 --- a/build-cargo-dep.sh +++ b/build-cargo-dep.sh @@ -18,7 +18,7 @@ if [ -z "$ARCH" ]; then fi DOCKER_PLATFORM="linux/${ARCH}" -if [ "$ARCH" = aarch64 ]; then +if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then DOCKER_PLATFORM="linux/arm64" elif [ "$ARCH" = x86_64 ]; then DOCKER_PLATFORM="linux/amd64" diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index 3ccaee4d6..cd29714b2 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -47,6 +47,7 @@ smartmontools socat sqlite3 squashfs-tools +squashfs-tools-ng sudo systemd systemd-resolved diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index ad7cd4bf3..45bed5fe9 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -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,11 @@ 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 + +# create kiosk script +cat > /home/kiosk/kiosk.sh << 'EOF' +#!/bin/sh while ! curl "http://localhost" > /dev/null; do sleep 1 done @@ -101,8 +99,7 @@ done killall firefox-esr ) & matchbox-window-manager -use_titlebar no & -firefox-esr http://localhost --profile $PROFILE -rm -rf $PROFILE +firefox-esr http://localhost --profile /home/kiosk/fx-profile EOF chmod +x /home/kiosk/kiosk.sh @@ -116,6 +113,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/container-runtime/download-base-image.sh b/container-runtime/download-base-image.sh index f7dc4c844..7fb134f31 100755 --- a/container-runtime/download-base-image.sh +++ b/container-runtime/download-base-image.sh @@ -16,4 +16,8 @@ elif [ "$_ARCH" = "aarch64" ]; then _ARCH=arm64 fi -curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output debian.${ARCH}.squashfs \ No newline at end of file +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/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/package-lock.json b/container-runtime/package-lock.json index e63bec6a1..2fddf23f2 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -15,24 +15,30 @@ "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", + "tslog": "^4.9.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/dist": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha6", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -48,1186 +54,1473 @@ "@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/@iarna/toml": { - "version": "2.2.5", - "license": "ISC" - }, - "node_modules/@mole-inc/bin-wrapper": { - "version": "8.0.1", + "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, - "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" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=6.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==", + "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": { - "@noble/hashes": "1.4.0" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=6.9.0" } }, - "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==", + "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": ">= 16" + "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": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", + "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, - "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "ms": "2.1.2" }, "engines": { - "node": ">= 8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", + "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, - "license": "MIT", - "engines": { - "node": ">= 8" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", + "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, - "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.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, - "license": "MIT", - "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@start9labs/start-sdk": { - "resolved": "../sdk/dist", - "link": true - }, - "node_modules/@swc/cli": { - "version": "0.1.65", + "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, - "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" - }, + "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": { - "spack": "bin/spack.js", - "swc": "bin/swc.js", - "swcx": "bin/swcx.js" + "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": ">= 12.13" - }, - "peerDependencies": { - "@swc/core": "^1.2.66", - "chokidar": "^3.5.1" + "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" }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@swc/core": { - "version": "1.5.28", + "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, - "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.8" + "@babel/types": "^7.24.7" }, "engines": { - "node": ">=10" + "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" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" + "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" }, - "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" + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@swc/helpers": "*" + "@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" }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.5.28", - "cpu": [ - "x64" - ], + "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, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.5.28", - "cpu": [ - "x64" - ], + "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, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", + "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, - "license": "Apache-2.0" + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@swc/types": { - "version": "0.1.8", + "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, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", + "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, - "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.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, - "license": "MIT" + "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/@types/cacheable-request": { - "version": "6.0.3", + "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, - "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.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, - "license": "MIT" + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/@types/keyv": { - "version": "3.1.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, - "license": "MIT", "dependencies": { - "@types/node": "*" + "color-name": "1.1.3" } }, - "node_modules/@types/node": { - "version": "20.14.2", + "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, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", + "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, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=4" } }, - "node_modules/accepts": { - "version": "1.3.8", - "license": "MIT", + "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": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "has-flag": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=4" } }, - "node_modules/arch": { - "version": "2.2.0", + "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, - "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" + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "license": "MIT" + "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/balanced-match": { - "version": "1.0.2", + "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, - "license": "MIT" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/bin-check": { - "version": "4.1.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": { - "execa": "^0.7.0", - "executable": "^4.1.0" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version": { - "version": "6.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, - "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "find-versions": "^5.0.0" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=12" + "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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version-check": { - "version": "5.1.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, - "license": "MIT", "dependencies": { - "bin-version": "^6.0.0", - "semver": "^7.5.3", - "semver-truncate": "^3.0.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/cross-spawn": { - "version": "7.0.3", + "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": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/execa": { - "version": "5.1.1", + "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", "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" + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/get-stream": { - "version": "6.0.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": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/is-stream": { - "version": "2.0.1", + "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" } }, - "node_modules/bin-version/node_modules/npm-run-path": { - "version": "4.0.1", + "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": { - "path-key": "^3.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/path-key": { - "version": "3.1.1", + "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" } }, - "node_modules/bin-version/node_modules/shebang-command": { - "version": "2.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": { - "shebang-regex": "^3.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/shebang-regex": { - "version": "3.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, - "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/bin-version/node_modules/which": { - "version": "2.0.2", + "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, - "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/body-parser": { - "version": "1.20.2", - "license": "MIT", + "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": { - "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" + "@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": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=6.9.0" } }, - "node_modules/brace-expansion": { - "version": "2.0.1", + "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, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/braces": { - "version": "3.0.3", + "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, - "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", + "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": ">= 0.8" + "node": ">=8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", + "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": { - "node": ">=10.6.0" + "node": ">=8" } }, - "node_modules/cacheable-request": { - "version": "7.0.4", + "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, - "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" + "@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": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.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, - "license": "MIT", "dependencies": { - "pump": "^3.0.0" + "@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" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/call-bind": { - "version": "1.0.7", - "license": "MIT", + "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": { - "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" + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/clone-response": { - "version": "1.0.3", + "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, - "license": "MIT", "dependencies": { - "mimic-response": "^1.0.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/commander": { - "version": "7.2.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, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "jest-get-type": "^29.6.3" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", + "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": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie": { - "version": "0.6.0", - "license": "MIT", + "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": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "5.1.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, - "license": "MIT", "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "@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/data-uri-to-buffer": { - "version": "4.0.1", - "license": "MIT", + "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": ">= 12" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/debug": { - "version": "2.6.9", - "license": "MIT", + "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": { - "ms": "2.0.0" + "@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/decompress-response": { - "version": "6.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, - "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "@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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.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, - "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", + "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, - "license": "MIT", + "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": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "license": "MIT", + "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": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.0.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "license": "MIT", + "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": ">= 0.8" + "node": ">=6.0.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "license": "MIT", + "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": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=6.0.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "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/end-of-stream": { - "version": "1.4.4", + "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, - "license": "MIT", "dependencies": { - "once": "^1.4.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/es-define-property": { - "version": "1.0.0", + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4" + "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": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "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/esbuild-plugin-resolve": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/escape-html": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "dev": true, - "license": "MIT", + "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": ">=12" + "node": ">= 16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/etag": { - "version": "1.8.1", + "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": ">= 0.6" + "node": ">= 8" } }, - "node_modules/execa": { - "version": "0.7.0", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", "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": ">= 8" } }, - "node_modules/executable": { - "version": "4.1.1", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", "dev": true, "license": "MIT", "dependencies": { - "pify": "^2.2.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/express": { - "version": "4.19.2", + "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", - "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": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/ext-list": { - "version": "2.2.2", + "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, - "license": "MIT", "dependencies": { - "mime-db": "^1.28.0" - }, - "engines": { - "node": ">=0.10.0" + "type-detect": "4.0.8" } }, - "node_modules/ext-name": { - "version": "5.0.0", + "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": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" + "@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": ">=4" + "node": ">= 12.13" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/fast-glob": { - "version": "3.3.2", + "node_modules/@swc/core": { + "version": "1.5.28", "dev": true, - "license": "MIT", + "hasInstallScript": true, + "license": "Apache-2.0", "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" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" }, "engines": { - "node": ">=8.6.0" + "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/fastq": { - "version": "1.17.1", + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.28", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } + "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" ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=10" } }, - "node_modules/file-type": { - "version": "17.1.6", + "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": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0-alpha.9", - "token-types": "^5.0.0-alpha.2" + "defer-to-connect": "^2.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" + "node": ">=10" } }, - "node_modules/filebrowser": { - "version": "1.0.0", - "license": "ISC", + "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": { - "commander": "^2.9.0", - "content-disposition": "^0.5.1", - "express": "^4.14.0" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/filebrowser/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/filename-reserved-regex": { - "version": "3.0.0", + "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, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "node_modules/filenamify": { - "version": "5.1.1", + "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, - "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" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", + "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, - "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" + "@babel/types": "^7.20.7" } }, - "node_modules/finalhandler": { - "version": "1.2.0", + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "dev": true, "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" + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" } }, - "node_modules/find-versions": { - "version": "5.1.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, - "license": "MIT", "dependencies": { - "semver-regex": "^4.0.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/node": "*" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "license": "MIT", + "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": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "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/fresh": { - "version": "0.5.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "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/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "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/get-intrinsic": { - "version": "1.2.4", + "node_modules/@types/keyv": { + "version": "3.1.4", + "dev": true, "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" + "@types/node": "*" } }, - "node_modules/get-stream": { - "version": "3.0.0", + "node_modules/@types/node": { + "version": "20.14.2", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "undici-types": "~5.26.4" } }, - "node_modules/gopd": { - "version": "1.0.1", + "node_modules/@types/responselike": { + "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@types/node": "*" } }, - "node_modules/got": { - "version": "11.8.6", + "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, - "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" + "@types/yargs-parser": "*" } }, - "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/@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/has-proto": { - "version": "1.0.3", + "node_modules/accepts": { + "version": "1.3.8", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, - "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": ">= 0.6" } }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", + "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": { - "function-bind": "^1.1.2" + "type-fest": "^0.21.3" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", + "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, - "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": ">=8" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", + "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, - "license": "MIT", "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=10.19.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/human-signals": { - "version": "2.1.0", + "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, - "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" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/ieee754": { - "version": "1.2.1", + "node_modules/arch": { + "version": "2.2.0", "dev": true, "funding": [ { @@ -1243,1215 +1536,6819 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" + "license": "MIT" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" + "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/is-extglob": { - "version": "2.1.1", + "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, - "license": "MIT", + "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": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", + "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": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@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": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-number": { - "version": "7.0.0", + "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, - "license": "MIT", + "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": ">=0.12.0" + "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", + "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, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/is-stream": { - "version": "1.1.0", + "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, - "license": "MIT", + "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": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/isexe": { - "version": "2.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, - "license": "ISC" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "license": "MIT", "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" + "@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/isomorphic-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", + "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": { - "whatwg-url": "^5.0.0" + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", + "node_modules/balanced-match": { + "version": "1.0.2", "dev": true, "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", + "node_modules/bin-check": { + "version": "4.1.0", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" } }, - "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", + "node_modules/bin-version": { + "version": "6.0.0", "dev": true, "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "4.1.5", + "node_modules/bin-version-check": { + "version": "5.1.0", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "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/media-typer": { - "version": "0.3.0", + "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": ">= 0.6" + "node": ">= 8" } }, - "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", + "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": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/methods": { - "version": "1.1.2", + "node_modules/bin-version/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.7", + "node_modules/bin-version/node_modules/is-stream": { + "version": "2.0.1", "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime": { - "version": "1.6.0", + "node_modules/bin-version/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "path-key": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/mime-db": { - "version": "1.52.0", + "node_modules/bin-version/node_modules/path-key": { + "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/mime-types": { - "version": "2.1.35", + "node_modules/bin-version/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", + "node_modules/bin-version/node_modules/shebang-regex": { + "version": "3.0.0", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/mimic-response": { - "version": "1.0.1", + "node_modules/bin-version/node_modules/which": { + "version": "2.0.2", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", + "node_modules/body-parser": { + "version": "1.20.2", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "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": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/ms": { - "version": "2.0.0", - "license": "MIT" + "node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "node_modules/negotiator": { - "version": "0.6.3", + "node_modules/braces": { + "version": "3.0.3", + "dev": true, "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/node-domexception": { - "version": "1.0.0", + "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": "github", - "url": "https://github.com/sponsors/jimmywarting" + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", - "url": "https://paypal.me/jimmywarting" + "url": "https://github.com/sponsors/ai" } ], - "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" + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "bin": { + "browserslist": "cli.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "dev": true, - "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/npm-run-path": { - "version": "2.0.2", + "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, - "license": "MIT", "dependencies": { - "path-key": "^2.0.0" + "fast-json-stable-stringify": "2.x" }, "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/object-inspect": { - "version": "1.13.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/on-finished": { - "version": "2.4.1", + "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", - "dependencies": { - "ee-first": "1.1.1" - }, "engines": { "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", + "node_modules/cacheable-lookup": { + "version": "5.0.4", "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" + "license": "MIT", + "engines": { + "node": ">=10.6.0" } }, - "node_modules/onetime": { - "version": "5.1.2", + "node_modules/cacheable-request": { + "version": "7.0.4", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "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": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/os-filter-obj": { - "version": "2.0.0", + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", "dev": true, "license": "MIT", "dependencies": { - "arch": "^2.1.0" + "pump": "^3.0.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "dev": true, - "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "dev": true, + "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": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "license": "MIT", + "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": ">= 0.8" + "node": ">=6" } }, - "node_modules/path-key": { - "version": "2.0.1", + "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, - "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "license": "MIT" + "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/peek-readable": { - "version": "5.0.0", + "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, - "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=14.16" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/picomatch": { - "version": "2.3.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, - "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=10" } }, - "node_modules/pify": { - "version": "2.3.0", + "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, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/prettier": { - "version": "3.3.2", + "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, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">=12" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", + "node_modules/clone-response": { + "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "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": { - "node": ">= 0.10" + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" } }, - "node_modules/pseudomap": { + "node_modules/collect-v8-coverage": { "version": "1.0.2", - "dev": true, - "license": "ISC" + "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/pump": { - "version": "3.0.0", + "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, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=7.0.0" } }, - "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/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/quick-lru": { - "version": "5.1.1", + "node_modules/commander": { + "version": "7.2.0", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, - "node_modules/range-parser": { - "version": "1.2.1", + "node_modules/content-type": { + "version": "1.0.5", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.2", + "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": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "@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": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/cross-spawn": { + "version": "5.1.0", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "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": ">= 6" + "node": ">= 12" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", + "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": { - "readable-stream": "^3.6.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", "dev": true, "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" + "engines": { + "node": ">=10" }, "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", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true } - ], - "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", + "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/semver": { - "version": "7.6.2", + "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, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/semver-regex": { - "version": "4.0.5", + "node_modules/defer-to-connect": { + "version": "2.0.1", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10" } }, - "node_modules/semver-truncate": { - "version": "3.0.0", - "dev": true, + "node_modules/define-data-property": { + "version": "1.1.4", "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/send": { - "version": "0.18.0", + "node_modules/depd": { + "version": "2.0.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": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.15.0", + "node_modules/destroy": { + "version": "1.2.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": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "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" - }, + "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": ">= 0.4" + "node": ">=8" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "1.2.0", + "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, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/shebang-regex": { - "version": "1.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, - "license": "MIT", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, "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" - }, + "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": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" + "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/slash": { - "version": "3.0.0", - "dev": true, + "node_modules/encodeurl": { + "version": "1.0.2", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/sort-keys": { - "version": "1.1.2", + "node_modules/end-of-stream": { + "version": "1.4.4", "dev": true, "license": "MIT", "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "once": "^1.4.0" } }, - "node_modules/sort-keys-length": { - "version": "1.0.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, - "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" + "is-arrayish": "^0.2.1" } }, - "node_modules/statuses": { - "version": "2.0.1", + "node_modules/es-define-property": { + "version": "1.0.0", "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, - "node_modules/string_decoder": { + "node_modules/es-errors": { "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/strip-final-newline": { + "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, - "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/strip-outer": { - "version": "2.0.0", + "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.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "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": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" + "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": ">=14.16" + "node": ">=4.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "optionalDependencies": { + "source-map": "~0.6.1" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" + "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": ">=8.0" + "node": ">=4" } }, - "node_modules/toidentifier": { - "version": "1.0.1", + "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": ">= 0.6" } }, - "node_modules/token-types": { - "version": "5.0.1", + "node_modules/execa": { + "version": "0.7.0", "dev": true, "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" + "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": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "node": ">=4" } }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, - "node_modules/trim-repeated": { - "version": "2.0.0", + "node_modules/executable": { + "version": "4.1.1", "dev": true, "license": "MIT", "dependencies": { - "escape-string-regexp": "^5.0.0" + "pify": "^2.2.0" }, "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/ts-matches": { - "version": "5.5.1", - "license": "MIT" + "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/tslib": { - "version": "2.6.3", - "license": "0BSD" + "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/type-is": { - "version": "1.6.18", + "node_modules/express": { + "version": "4.19.2", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "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.6" + "node": ">= 0.10.0" } }, - "node_modules/typescript": { - "version": "5.4.5", + "node_modules/ext-list": { + "version": "2.2.2", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" }, "engines": { - "node": ">=14.17" + "node": ">=0.10.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", + "node_modules/ext-name": { + "version": "5.0.0", "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", + "node_modules/fast-glob": { + "version": "3.3.2", "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", "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": ">= 0.4.0" + "node": ">=8.6.0" } }, - "node_modules/vary": { - "version": "1.1.2", + "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": ">= 0.8" + "node": "^12.20 || >= 14.13" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", + "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": ">= 8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" + "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/whatwg-fetch": { - "version": "3.6.20", + "node_modules/filebrowser/node_modules/commander": { + "version": "2.20.3", "license": "MIT" }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "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": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "minimatch": "^5.0.1" } }, - "node_modules/which": { - "version": "1.3.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, - "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "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/tslog": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", + "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, + "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": "^4.0.3", + "peggy": "^3.0.2", + "prettier": "^3.2.5", + "ts-jest": "^29.0.5", + "ts-matches": "^5.5.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" }, - "bin": { - "which": "bin/which" + "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" + } + } } }, - "node_modules/wrappy": { - "version": "1.0.2", + "glob-parent": { + "version": "5.1.2", "dev": true, - "license": "ISC" + "requires": { + "is-glob": "^4.0.1" + } }, - "node_modules/yallist": { - "version": "2.1.2", - "dev": true, - "license": "ISC" + "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 }, - "node_modules/yaml": { - "version": "2.4.5", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" + "gopd": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.1.3" } - } - }, - "dependencies": { - "@iarna/toml": { - "version": "2.2.5" }, - "@mole-inc/bin-wrapper": { - "version": "8.0.1", + "got": { + "version": "11.8.6", "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" + "@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" } }, - "@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==", + "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": { - "@noble/hashes": "1.4.0" + "es-define-property": "^1.0.0" } }, - "@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + "has-proto": { + "version": "1.0.3" }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, + "has-symbols": { + "version": "1.0.3" + }, + "hasown": { + "version": "2.0.2", "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "function-bind": "^1.1.2" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", + "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 }, - "@nodelib/fs.walk": { - "version": "1.2.8", + "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": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" } }, - "@sindresorhus/is": { - "version": "4.6.0", + "human-signals": { + "version": "2.1.0", "dev": true }, - "@start9labs/start-sdk": { - "version": "file:../sdk/dist", + "iconv-lite": { + "version": "0.4.24", "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": "^4.0.3", - "prettier": "^3.2.5", - "ts-jest": "^29.0.5", - "ts-matches": "^5.5.1", - "ts-node": "^10.9.1", - "tsx": "^4.7.1", - "typescript": "^5.0.4", - "yaml": "^2.2.2" + "safer-buffer": ">= 2.1.2 < 3" } }, - "@swc/cli": { - "version": "0.1.65", + "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": { - "@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" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" } }, - "@swc/core": { - "version": "1.5.28", + "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": { - "@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" + "once": "^1.3.0", + "wrappy": "1" } }, - "@swc/core-linux-x64-gnu": { - "version": "1.5.28", - "dev": true, - "optional": true + "inherits": { + "version": "2.0.4" }, - "@swc/core-linux-x64-musl": { - "version": "1.5.28", - "dev": true, - "optional": true + "ipaddr.js": { + "version": "1.9.1" }, - "@swc/counter": { - "version": "0.1.3", + "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 }, - "@swc/types": { - "version": "0.1.8", + "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": { - "@swc/counter": "^0.1.3" + "hasown": "^2.0.2" } }, - "@szmarczak/http-timer": { - "version": "4.0.6", + "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": { - "defer-to-connect": "^2.0.0" + "is-extglob": "^2.1.1" } }, - "@tokenizer/token": { - "version": "0.3.0", + "is-number": { + "version": "7.0.0", "dev": true }, - "@types/cacheable-request": { - "version": "6.0.3", - "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": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, - "@types/http-cache-semantics": { - "version": "4.0.4", + "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 }, - "@types/keyv": { - "version": "3.1.4", + "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": { - "@types/node": "*" + "@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" } }, - "@types/node": { - "version": "20.14.2", + "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": { - "undici-types": "~5.26.4" + "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 + } } }, - "@types/responselike": { - "version": "1.0.3", + "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": { - "@types/node": "*" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "accepts": { - "version": "1.3.8", + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "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" + } + } } }, - "arch": { - "version": "2.2.0", - "dev": true - }, - "array-flatten": { - "version": "1.1.1" - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "bin-check": { - "version": "4.1.0", + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "requires": { - "execa": "^0.7.0", - "executable": "^4.1.0" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" } }, - "bin-version": { - "version": "6.0.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", - "find-versions": "^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", @@ -2461,6 +8358,8 @@ }, "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", @@ -2476,14 +8375,20 @@ }, "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" @@ -2491,10 +8396,14 @@ }, "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" @@ -2502,528 +8411,479 @@ }, "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", - "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" - } - }, - "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" - } - }, - "clone-response": { - "version": "1.0.3", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "commander": { - "version": "7.2.0", - "dev": true - }, - "content-disposition": { - "version": "0.5.4", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.5" - }, - "cookie": { - "version": "0.6.0" - }, - "cookie-signature": { - "version": "1.0.6" - }, - "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 - } - } - }, - "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" - }, - "ee-first": { - "version": "1.1.1" - }, - "encodeurl": { - "version": "1.0.2" - }, - "end-of-stream": { - "version": "1.4.4", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "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" - }, - "escape-html": { - "version": "1.0.3" - }, - "escape-string-regexp": { - "version": "5.0.0", - "dev": true - }, - "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" + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "executable": { - "version": "4.1.1", + "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": { - "pify": "^2.2.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", + "@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": { - "mime-db": "^1.28.0" - } - }, - "ext-name": { - "version": "5.0.0", + "@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": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" - } - }, - "fast-glob": { - "version": "3.3.2", + "@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": { - "@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" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, - "fastq": { - "version": "1.17.1", + "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": { - "reusify": "^1.0.4" - } - }, - "fetch-blob": { - "version": "3.2.0", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "detect-newline": "^3.0.0" } }, - "file-type": { - "version": "17.1.6", + "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": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0-alpha.9", - "token-types": "^5.0.0-alpha.2" + "@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" } }, - "filebrowser": { - "version": "1.0.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": { - "commander": "^2.9.0", - "content-disposition": "^0.5.1", - "express": "^4.14.0" - }, - "dependencies": { - "commander": { - "version": "2.20.3" - } + "@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" } }, - "filename-reserved-regex": { - "version": "3.0.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 }, - "filenamify": { - "version": "5.1.1", + "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": { - "filename-reserved-regex": "^3.0.0", - "strip-outer": "^2.0.0", - "trim-repeated": "^2.0.0" - } - }, - "fill-range": { - "version": "7.1.1", + "@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": { - "to-regex-range": "^5.0.1" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, - "finalhandler": { - "version": "1.2.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": { - "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" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, - "find-versions": { - "version": "5.1.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": { - "semver-regex": "^4.0.5" - } - }, - "formdata-polyfill": { - "version": "4.0.10", + "@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": { - "fetch-blob": "^3.1.2" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" } }, - "forwarded": { - "version": "0.2.0" - }, - "fresh": { - "version": "0.5.2" - }, - "function-bind": { - "version": "1.1.2" - }, - "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" - } + "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": {} }, - "get-stream": { - "version": "3.0.0", + "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 }, - "glob-parent": { - "version": "5.1.2", + "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": { - "is-glob": "^4.0.1" - } - }, - "gopd": { - "version": "1.0.1", + "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": { - "get-intrinsic": "^1.1.3" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" } }, - "got": { - "version": "11.8.6", + "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": { - "@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" - } - }, - "has-property-descriptors": { - "version": "1.0.2", + "@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": { - "es-define-property": "^1.0.0" - } - }, - "has-proto": { - "version": "1.0.3" - }, - "has-symbols": { - "version": "1.0.3" - }, - "hasown": { - "version": "2.0.2", + "@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": { - "function-bind": "^1.1.2" + "@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" } }, - "http-cache-semantics": { - "version": "4.1.1", - "dev": true - }, - "http-errors": { - "version": "2.0.0", + "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": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "@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 + } } }, - "http2-wrapper": { - "version": "1.0.3", + "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": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" + "@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" } }, - "human-signals": { - "version": "2.1.0", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", + "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": { - "safer-buffer": ">= 2.1.2 < 3" + "@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" + } + } } }, - "ieee754": { - "version": "1.2.1", - "dev": true - }, - "inherits": { - "version": "2.0.4" - }, - "ipaddr.js": { - "version": "1.9.1" - }, - "is-extglob": { - "version": "2.1.1", + "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 }, - "is-glob": { - "version": "4.0.3", + "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": { - "is-extglob": "^2.1.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, - "is-number": { - "version": "7.0.0", + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "is-plain-obj": { - "version": "1.1.0", + "json-buffer": { + "version": "3.0.1", "dev": true }, - "is-stream": { - "version": "1.1.0", + "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 }, - "isexe": { - "version": "2.0.0", + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, - "isomorphic-fetch": { - "version": "3.0.0", + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", "requires": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" }, "dependencies": { - "node-fetch": { - "version": "2.7.0", - "requires": { - "whatwg-url": "^5.0.0" - } + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==" } } }, - "json-buffer": { - "version": "3.0.1", - "dev": true - }, "keyv": { "version": "4.5.4", "dev": true, @@ -3031,6 +8891,48 @@ "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", @@ -3048,6 +8950,30 @@ "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" }, @@ -3103,6 +9029,12 @@ "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" }, @@ -3117,6 +9049,24 @@ "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 @@ -3151,6 +9101,19 @@ "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, @@ -3166,13 +9129,78 @@ "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" }, @@ -3180,6 +9208,12 @@ "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 @@ -3188,10 +9222,59 @@ "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": { @@ -3211,6 +9294,12 @@ "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": { @@ -3237,6 +9326,12 @@ "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, @@ -3253,10 +9348,48 @@ "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, @@ -3366,6 +9499,12 @@ "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 @@ -3388,6 +9527,55 @@ "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" }, @@ -3398,6 +9586,42 @@ "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 @@ -3406,6 +9630,12 @@ "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 @@ -3418,6 +9648,65 @@ "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, @@ -3446,12 +9735,54 @@ "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" }, + "tslog": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", + "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==" + }, + "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": { @@ -3463,6 +9794,11 @@ "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 @@ -3470,6 +9806,16 @@ "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 @@ -3477,9 +9823,29 @@ "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" }, @@ -3503,16 +9869,75 @@ "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 index 515f50ee7..0a8e4afa8 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -6,7 +6,8 @@ "scripts": { "check": "tsc --noEmit", "build": "prettier . '!tmp/**' --write && rm -rf dist && tsc", - "tsc": "rm -rf dist; tsc" + "tsc": "rm -rf dist; tsc", + "test": "jest -c ./jest.config.js" }, "author": "", "prettier": { @@ -17,12 +18,13 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@start9labs/start-sdk": "file:../sdk/dist", - "@noble/hashes": "^1.4.0", "@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", @@ -33,8 +35,12 @@ "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/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts new file mode 100644 index 000000000..e0390b1e1 --- /dev/null +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -0,0 +1,316 @@ +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 { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +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 | null +} + +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 }, + }) + "\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 }), + ) + 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) + }) + }) + } + +function makeEffects(context: EffectContext): Effects { + const rpcRound = rpcRoundFor(context.procedureId) + const self: Effects = { + bind(...[options]: Parameters) { + return rpcRound("bind", { + ...options, + stack: new Error().stack, + }) as ReturnType + }, + clearBindings(...[]: Parameters) { + return rpcRound("clear-bindings", {}) as ReturnType< + T.Effects["clearBindings"] + > + }, + clearServiceInterfaces( + ...[]: Parameters + ) { + return rpcRound("clear-service-interfaces", {}) as ReturnType< + T.Effects["clearServiceInterfaces"] + > + }, + getInstalledPackages(...[]: Parameters) { + return rpcRound("get-installed-packages", {}) as ReturnType< + T.Effects["getInstalledPackages"] + > + }, + subcontainer: { + createFs(options: { imageId: 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"] + > + }, + }, + executeAction(...[options]: Parameters) { + return rpcRound("execute-action", options) as ReturnType< + T.Effects["executeAction"] + > + }, + exportAction(...[options]: Parameters) { + return rpcRound("export-action", options) as ReturnType< + T.Effects["exportAction"] + > + }, + 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"] + > + }, + getConfigured(...[]: Parameters) { + return rpcRound("get-configured", {}) as ReturnType< + T.Effects["getConfigured"] + > + }, + 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 + }, + + getPrimaryUrl(...[options]: Parameters) { + return rpcRound("get-primary-url", { + ...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 + }, + clearActions(...[]: Parameters) { + return rpcRound("clear-actions", {}) as ReturnType< + T.Effects["clearActions"] + > + }, + restart(...[]: Parameters) { + return rpcRound("restart", {}) as ReturnType + }, + setConfigured(...[configured]: Parameters) { + return rpcRound("set-configured", { configured }) as ReturnType< + T.Effects["setConfigured"] + > + }, + 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"] + > + }, + + 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 +} + +export function makeProcedureEffects(procedureId: string): Effects { + return makeEffects({ procedureId, callbacks: null }) +} + +export function makeMainEffects(): MainEffects { + const rpcRound = rpcRoundFor(null) + return { + _type: "main", + clearCallbacks: () => { + return rpcRound("clearCallbacks", {}) as Promise + }, + ...makeEffects({ procedureId: null, callbacks: new CallbackHolder() }), + } +} diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts deleted file mode 100644 index 1996af0fd..000000000 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { types as T } 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" -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" -const MAIN = "/main" as const -let hostSystemId = 0 -export const hostSystemStartOs = - (callbackHolder: CallbackHolder) => - (procedureId: null | string): Effects => { - const rpcRound = ( - method: K, - params: Record, - ) => { - const id = hostSystemId++ - const client = net.createConnection({ path: SOCKET_PATH }, () => { - client.write( - JSON.stringify({ - id, - method, - params: { ...params, procedureId: procedureId }, - }) + "\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({ method, params, hostSystemStartOs: true }) - if (string.test(res.error.data)) { - message += ": " + res.error.data - console.error(res.error.data) - } else { - if (res.error.data?.details) { - message += ": " + res.error.data.details - console.error(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) - }) - }) - } - const self: Effects = { - bind(...[options]: Parameters) { - return rpcRound("bind", { - ...options, - stack: new Error().stack, - }) as ReturnType - }, - clearBindings(...[]: Parameters) { - return rpcRound("clearBindings", {}) as ReturnType< - T.Effects["clearBindings"] - > - }, - clearServiceInterfaces( - ...[]: Parameters - ) { - return rpcRound("clearServiceInterfaces", {}) as ReturnType< - T.Effects["clearServiceInterfaces"] - > - }, - createOverlayedImage(options: { - imageId: string - }): Promise<[string, string]> { - return rpcRound("createOverlayedImage", options) as ReturnType< - T.Effects["createOverlayedImage"] - > - }, - destroyOverlayedImage(options: { guid: string }): Promise { - return rpcRound("destroyOverlayedImage", options) as ReturnType< - T.Effects["destroyOverlayedImage"] - > - }, - executeAction(...[options]: Parameters) { - return rpcRound("executeAction", options) as ReturnType< - T.Effects["executeAction"] - > - }, - exists(...[packageId]: Parameters) { - return rpcRound("exists", packageId) as ReturnType - }, - exportAction(...[options]: Parameters) { - return rpcRound("exportAction", options) as ReturnType< - T.Effects["exportAction"] - > - }, - exportServiceInterface: (( - ...[options]: Parameters - ) => { - return rpcRound("exportServiceInterface", options) as ReturnType< - T.Effects["exportServiceInterface"] - > - }) as Effects["exportServiceInterface"], - exposeForDependents( - ...[options]: Parameters - ) { - return rpcRound("exposeForDependents", options) as ReturnType< - T.Effects["exposeForDependents"] - > - }, - getConfigured(...[]: Parameters) { - return rpcRound("getConfigured", {}) as ReturnType< - T.Effects["getConfigured"] - > - }, - getContainerIp(...[]: Parameters) { - return rpcRound("getContainerIp", {}) as ReturnType< - T.Effects["getContainerIp"] - > - }, - getHostInfo: ((...[allOptions]: any[]) => { - const options = { - ...allOptions, - callback: callbackHolder.addCallback(allOptions.callback), - } - return rpcRound("getHostInfo", options) as ReturnType< - T.Effects["getHostInfo"] - > as any - }) as Effects["getHostInfo"], - getServiceInterface( - ...[options]: Parameters - ) { - return rpcRound("getServiceInterface", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - - getPrimaryUrl(...[options]: Parameters) { - return rpcRound("getPrimaryUrl", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - getServicePortForward( - ...[options]: Parameters - ) { - return rpcRound("getServicePortForward", options) as ReturnType< - T.Effects["getServicePortForward"] - > - }, - getSslCertificate( - options: Parameters[0], - ) { - return rpcRound("getSslCertificate", options) as ReturnType< - T.Effects["getSslCertificate"] - > - }, - getSslKey(options: Parameters[0]) { - return rpcRound("getSslKey", options) as ReturnType< - T.Effects["getSslKey"] - > - }, - getSystemSmtp(...[options]: Parameters) { - return rpcRound("getSystemSmtp", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - listServiceInterfaces( - ...[options]: Parameters - ) { - return rpcRound("listServiceInterfaces", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - mount(...[options]: Parameters) { - return rpcRound("mount", options) as ReturnType - }, - removeAction(...[options]: Parameters) { - return rpcRound("removeAction", options) as ReturnType< - T.Effects["removeAction"] - > - }, - removeAddress(...[options]: Parameters) { - return rpcRound("removeAddress", options) as ReturnType< - T.Effects["removeAddress"] - > - }, - restart(...[]: Parameters) { - return rpcRound("restart", {}) as ReturnType - }, - running(...[packageId]: Parameters) { - return rpcRound("running", { packageId }) as ReturnType< - T.Effects["running"] - > - }, - // runRsync(...[options]: Parameters) { - // - // return rpcRound('executeAction', options) as ReturnType - // - // return rpcRound('executeAction', options) as ReturnType - // } - setConfigured(...[configured]: Parameters) { - return rpcRound("setConfigured", { configured }) as ReturnType< - T.Effects["setConfigured"] - > - }, - setDependencies( - dependencies: Parameters[0], - ): ReturnType { - return rpcRound("setDependencies", dependencies) as ReturnType< - T.Effects["setDependencies"] - > - }, - checkDependencies( - options: Parameters[0], - ): ReturnType { - return rpcRound("checkDependencies", options) as ReturnType< - T.Effects["checkDependencies"] - > - }, - getDependencies(): ReturnType { - return rpcRound("getDependencies", {}) as ReturnType< - T.Effects["getDependencies"] - > - }, - setHealth(...[options]: Parameters) { - return rpcRound("setHealth", options) as ReturnType< - T.Effects["setHealth"] - > - }, - - setMainStatus(o: { status: "running" | "stopped" }): Promise { - return rpcRound("setMainStatus", o) as ReturnType< - T.Effects["setHealth"] - > - }, - - shutdown(...[]: Parameters) { - return rpcRound("shutdown", {}) as ReturnType - }, - stopped(...[packageId]: Parameters) { - return rpcRound("stopped", { packageId }) as ReturnType< - T.Effects["stopped"] - > - }, - store: { - get: async (options: any) => - rpcRound("getStore", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as any, - set: async (options: any) => - rpcRound("setStore", options) as ReturnType< - T.Effects["store"]["set"] - >, - } as T.Effects["store"], - } - return self - } diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 04e9bc40f..860f1c066 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -15,15 +15,16 @@ import { } from "ts-matches" import { types as T } from "@start9labs/start-sdk" -import * as CP from "child_process" -import * as Mod from "module" import * as fs from "fs" import { CallbackHolder } from "../Models/CallbackHolder" import { AllGetDependencies } from "../Interfaces/AllGetDependencies" -import { HostSystem } from "../Interfaces/HostSystem" -import { jsonPath } from "../Models/JsonPath" -import { System } from "../Interfaces/System" +import { jsonPath, unNestPath } from "../Models/JsonPath" +import { RunningMain, System } from "../Interfaces/System" +import { + MakeMainEffects, + MakeProcedureEffects, +} from "../Interfaces/MakeEffects" type MaybePromise = T | Promise export const matchRpcResult = anyOf( object({ result: any }), @@ -45,12 +46,14 @@ export const matchRpcResult = anyOf( }), ) export type RpcResult = typeof matchRpcResult._TYPE -type SocketResponse = { jsonrpc: "2.0"; id: IdType } & RpcResult +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 const runType = object({ @@ -80,7 +83,6 @@ const sandboxRunType = object({ ), }) const callbackType = object({ - id: idType, method: literal("callback"), params: object({ callback: number, @@ -91,6 +93,14 @@ const initType = object({ id: idType, method: literal("init"), }) +const startType = object({ + id: idType, + method: literal("start"), +}) +const stopType = object({ + id: idType, + method: literal("stop"), +}) const exitType = object({ id: idType, method: literal("exit"), @@ -104,33 +114,40 @@ const evalType = object({ }) const jsonParse = (x: string) => JSON.parse(x) -function reduceMethod( - methodArgs: object, - effects: HostSystem, -): (previousValue: any, currentValue: string) => any { - return (x: any, method: string) => - Promise.resolve(x) - .then((x) => x[method]) - .then((x) => - typeof x !== "function" - ? x - : x({ - ...methodArgs, - effects, - }), + +const handleRpc = (id: IdType, result: Promise) => + result + .then((result) => ({ + 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 _effects: HostSystem | undefined + private _makeProcedureEffects: MakeProcedureEffects | undefined + private _makeMainEffects: MakeMainEffects | undefined - constructor( - readonly getDependencies: AllGetDependencies, - private callbacks = new CallbackHolder(), - ) { + constructor(readonly getDependencies: AllGetDependencies) { if (!fs.existsSync(SOCKET_PARENT)) { fs.mkdirSync(SOCKET_PARENT, { recursive: true }) } @@ -165,8 +182,13 @@ export class RpcListener { code: 1, }, }) - const writeDataToSocket = (x: SocketResponse) => - new Promise((resolve) => s.write(JSON.stringify(x) + "\n", resolve)) + 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()) @@ -181,107 +203,114 @@ export class RpcListener { }) } - private get effects() { - return this.getDependencies.hostSystem()(this.callbacks) - } - private get system() { if (!this._system) throw new Error("System not initialized") return this._system } + private get makeProcedureEffects() { + if (!this._makeProcedureEffects) { + this._makeProcedureEffects = this.getDependencies.makeProcedureEffects() + } + return this._makeProcedureEffects + } + + private get makeMainEffects() { + if (!this._makeMainEffects) { + this._makeMainEffects = this.getDependencies.makeMainEffects() + } + return this._makeMainEffects + } + private dealWithInput(input: unknown): MaybePromise { return matches(input) - .when(some(runType, sandboxRunType), async ({ id, params }) => { + .when(runType, async ({ id, params }) => { const system = this.system const procedure = jsonPath.unsafeCast(params.procedure) - return system - .execute(this.effects, { - id: params.id, - procedure, - input: params.input, - timeout: params.timeout, - }) - .then((result) => ({ - 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 effects = this.getDependencies.makeProcedureEffects()(params.id) + const input = params.input + const timeout = params.timeout + const result = getResult(procedure, system, effects, timeout, input) + + return handleRpc(id, result) }) - .when(callbackType, async ({ id, params: { callback, args } }) => - Promise.resolve(this.callbacks.callCallback(callback, args)) - .then((result) => ({ - jsonrpc, - id, - result, - })) - .catch((error) => ({ - jsonrpc, - id, + .when(sandboxRunType, async ({ id, params }) => { + const system = this.system + const procedure = jsonPath.unsafeCast(params.procedure) + const effects = this.makeProcedureEffects(params.id) + const result = getResult( + procedure, + system, + effects, + params.input, + params.input, + ) - error: { - code: 0, - message: typeof error, - data: { - details: error?.message ?? String(error), - debug: error?.stack, - }, - }, - })), - ) + return handleRpc(id, result) + }) + .when(callbackType, async ({ params: { callback, args } }) => { + this.system.callCallback(callback, args) + return null + }) + .when(startType, async ({ id }) => { + return handleRpc( + id, + this.system + .start(this.makeMainEffects()) + .then((result) => ({ result })), + ) + }) + .when(stopType, async ({ id }) => { + return handleRpc( + id, + this.system.stop().then((result) => ({ result })), + ) + }) .when(exitType, async ({ id }) => { - if (this._system) await this._system.exit(this.effects(null)) - delete this._system - delete this._effects - - return { - jsonrpc, + return handleRpc( id, - result: null, - } + (async () => { + if (this._system) await this._system.exit() + })().then((result) => ({ result })), + ) }) .when(initType, async ({ id }) => { - this._system = await this.getDependencies.system() - - return { - jsonrpc, + return handleRpc( id, - result: null, - } + (async () => { + if (!this._system) { + const system = await this.getDependencies.system() + await system.containerInit() + this._system = system + } + })().then((result) => ({ result })), + ) }) .when(evalType, async ({ id, params }) => { - const result = await new Function( - `return (async () => { return (${params.script}) }).call(this)`, - ).call({ - listener: this, - require: require, - }) - return { - jsonrpc, + return handleRpc( id, - result: !["string", "number", "boolean", "null", "object"].includes( - typeof result, - ) - ? null - : result, - } + (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, method }) => ({ jsonrpc, @@ -313,3 +342,97 @@ export class RpcListener { }) } } +function getResult( + procedure: typeof jsonPath._TYPE, + system: System, + effects: T.Effects, + timeout: number | undefined, + input: any, +) { + const ensureResultTypeShape = ( + result: + | void + | T.ConfigRes + | T.PropertiesReturn + | T.ActionMetadata[] + | T.ActionResult, + ): { result: any } => { + if (isResult(result)) return result + return { result } + } + return (async () => { + switch (procedure) { + case "/backup/create": + return system.createBackup(effects, timeout || null) + case "/backup/restore": + return system.restoreBackup(effects, timeout || null) + case "/config/get": + return system.getConfig(effects, timeout || null) + case "/config/set": + return system.setConfig(effects, input, timeout || null) + case "/properties": + return system.properties(effects, timeout || null) + case "/actions/metadata": + return system.actionsMetadata(effects) + case "/init": + return system.packageInit( + effects, + string.optional().unsafeCast(input), + timeout || null, + ) + case "/uninit": + return system.packageUninit( + effects, + string.optional().unsafeCast(input), + timeout || null, + ) + default: + const procedures = unNestPath(procedure) + switch (true) { + case procedures[1] === "actions" && procedures[3] === "get": + return system.action(effects, procedures[2], input, timeout || null) + case procedures[1] === "actions" && procedures[3] === "run": + return system.action(effects, procedures[2], input, timeout || null) + case procedures[1] === "dependencies" && procedures[3] === "query": + return system.dependenciesAutoconfig( + effects, + procedures[2], + input, + timeout || null, + ) + + case procedures[1] === "dependencies" && procedures[3] === "update": + return system.dependenciesAutoconfig( + effects, + procedures[2], + 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 index c06395a17..805f9b531 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -1,53 +1,83 @@ import * as fs from "fs/promises" import * as cp from "child_process" -import { Overlay, types as T } from "@start9labs/start-sdk" +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/cjs/lib/util/SubContainer" export const exec = promisify(cp.exec) export const execFile = promisify(cp.execFile) export class DockerProcedureContainer { - private constructor(readonly overlay: Overlay) {} - // static async readonlyOf(data: DockerProcedure) { - // return DockerProcedureContainer.of(data, ["-o", "ro"]) - // } + private constructor(private readonly subcontainer: ExecSpawnable) {} + static async of( effects: T.Effects, + packageId: string, + data: DockerProcedure, + volumes: { [id: VolumeId]: Volume }, + options: { subcontainer?: ExecSpawnable } = {}, + ) { + const subcontainer = + options?.subcontainer ?? + (await DockerProcedureContainer.createSubContainer( + effects, + packageId, + data, + volumes, + )) + return new DockerProcedureContainer(subcontainer) + } + static async createSubContainer( + effects: T.Effects, + packageId: string, data: DockerProcedure, volumes: { [id: VolumeId]: Volume }, ) { - const overlay = await Overlay.of(effects, { id: data.image }) + const subcontainer = await SubContainer.of(effects, { id: data.image }) if (data.mounts) { const mounts = data.mounts for (const mount in mounts) { const path = mounts[mount].startsWith("/") - ? `${overlay.rootfs}${mounts[mount]}` - : `${overlay.rootfs}/${mounts[mount]}` + ? `${subcontainer.rootfs}${mounts[mount]}` + : `${subcontainer.rootfs}/${mounts[mount]}` await fs.mkdir(path, { recursive: true }) const volumeMount = volumes[mount] if (volumeMount.type === "data") { - await overlay.mount( + await subcontainer.mount( { type: "volume", id: mount, subpath: null, readonly: false }, mounts[mount], ) } else if (volumeMount.type === "assets") { - await overlay.mount( + await subcontainer.mount( { type: "assets", id: mount, subpath: null }, mounts[mount], ) } else if (volumeMount.type === "certificate") { - volumeMount + 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({ - packageId: null, - hostId: volumeMount["interface-id"], - algorithm: null, + hostnames, }) const key = await effects.getSslKey({ - packageId: null, - hostId: volumeMount["interface-id"], - algorithm: null, + hostnames, }) await fs.writeFile( `${path}/${volumeMount["interface-id"]}.cert.pem`, @@ -58,35 +88,47 @@ export class DockerProcedureContainer { 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"], - }, - }) + 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 overlay.mount({ type: "backup", subpath: null }, mounts[mount]) + await subcontainer.mount( + { type: "backup", subpath: null }, + mounts[mount], + ) } } } - - return new DockerProcedureContainer(overlay) + return subcontainer } - async exec(commands: string[]) { + async exec( + commands: string[], + options?: CommandOptions & ExecOptions, + timeoutMs?: number | null, + ) { try { - return await this.overlay.exec(commands) + return await this.subcontainer.exec(commands, options, timeoutMs) } finally { - await this.overlay.destroy() + await this.subcontainer.destroy?.() } } - async execFail(commands: string[], timeoutMs: number | null) { + async execFail( + commands: string[], + timeoutMs: number | null, + options?: CommandOptions & ExecOptions, + ) { try { - const res = await this.overlay.exec(commands, {}, timeoutMs) + const res = await this.subcontainer.exec(commands, options, timeoutMs) if (res.exitCode !== 0) { const codeOrSignal = res.exitCode !== null @@ -98,11 +140,11 @@ export class DockerProcedureContainer { } return res } finally { - await this.overlay.destroy() + await this.subcontainer.destroy?.() } } async spawn(commands: string[]): Promise { - return await this.overlay.spawn(commands) + 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 index 975bb52ca..cae3405b9 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -1,10 +1,12 @@ import { polyfillEffects } from "./polyfillEffects" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { SystemForEmbassy } from "." -import { hostSystemStartOs } from "../../HostSystemStartOs" -import { Daemons, T, daemons } from "@start9labs/start-sdk" +import { T, utils } from "@start9labs/start-sdk" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" import { Effects } from "../../../Models/Effects" +import { off } from "node:process" +import { CommandController } from "@start9labs/start-sdk/cjs/lib/mainFn/CommandController" +import { asError } from "@start9labs/start-sdk/cjs/lib/util" const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000 @@ -14,24 +16,31 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000 * Also, this has an ability to clean itself up too if need be. */ export class MainLoop { - private healthLoops: - | { - name: string - interval: NodeJS.Timeout - }[] - | undefined + get mainSubContainerHandle() { + return this.mainEvent?.daemon?.subContainerHandle + } + private healthLoops?: { + name: string + interval: NodeJS.Timeout + }[] + + private mainEvent?: { + daemon: Daemon + } - private mainEvent: - | Promise<{ - daemon: Daemon - }> - | undefined - constructor( + private constructor( readonly system: SystemForEmbassy, readonly effects: Effects, - ) { - this.healthLoops = this.constructHealthLoops() - this.mainEvent = this.constructMainEvent() + ) {} + + 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() { @@ -44,22 +53,32 @@ export class MainLoop { await this.setupInterfaces(effects) await effects.setMainStatus({ status: "running" }) const jsMain = (this.system.moduleCode as any)?.jsMain - const dockerProcedureContainer = await DockerProcedureContainer.of( - effects, - this.system.manifest.main, - this.system.manifest.volumes, - ) if (jsMain) { throw new Error("Unreachable") } - const daemon = await Daemon.of()( - this.effects, - { id: this.system.manifest.main.image }, - currentCommand, - { - overlay: dockerProcedureContainer.overlay, - }, - ) + const daemon = new Daemon(async () => { + const subcontainer = await DockerProcedureContainer.createSubContainer( + effects, + this.system.manifest.id, + this.system.manifest.main, + this.system.manifest.volumes, + ) + 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, @@ -115,7 +134,9 @@ export class MainLoop { const main = await mainEvent delete this.mainEvent delete this.healthLoops - await main?.daemon.stop().catch((e) => console.error(e)) + await main?.daemon + .stop() + .catch((e) => console.error(`Main loop error`, utils.asError(e))) this.effects.setMainStatus({ status: "stopped" }) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) } @@ -126,26 +147,48 @@ export class MainLoop { 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(asError(e))) const interval = setInterval(async () => { const actionProcedure = value const timeChanged = Date.now() - start if (actionProcedure.type === "docker") { - const container = await DockerProcedureContainer.of( - effects, - actionProcedure, - manifest.volumes, + const subcontainer = actionProcedure.inject + ? this.mainSubContainerHandle + : undefined + // prettier-ignore + const container = + await DockerProcedureContainer.of( + effects, + manifest.id, + actionProcedure, + manifest.volumes, + { + subcontainer, + } + ) + const env: Record = actionProcedure.inject + ? { + HOME: "/root", + } + : {} + const executed = await container.exec( + [actionProcedure.entrypoint, ...actionProcedure.args], + { input: JSON.stringify(timeChanged), env }, ) - const executed = await container.exec([ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(timeChanged), - ]) + if (executed.exitCode === 0) { await effects.setHealth({ id: healthId, name: value.name, result: "success", - message: actionProcedure["success-message"], + message: actionProcedure["success-message"] ?? null, }) return } @@ -190,6 +233,18 @@ export class MainLoop { }) 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, 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..01e2d0763 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -0,0 +1,1068 @@ +// 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", + "required": true, + "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", + "required": true, + "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", + "required": false, + "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", + "required": true, + "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", + "required": false, + "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", + "required": true, + "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 index 3a8d4f4b2..9e6382ca7 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1,10 +1,10 @@ -import { types as T, utils, EmVer } from "@start9labs/start-sdk" +import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk" import * as fs from "fs/promises" import { polyfillEffects } from "./polyfillEffects" import { Duration, duration, fromDuration } from "../../../Models/Duration" -import { System } from "../../../Interfaces/System" -import { matchManifest, Manifest, Procedure } from "./matchManifest" +import { System, Procedure } from "../../../Interfaces/System" +import { matchManifest, Manifest } from "./matchManifest" import * as childProcess from "node:child_process" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { promisify } from "node:util" @@ -27,7 +27,6 @@ import { Parser, array, } from "ts-matches" -import { hostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { RpcResult, matchRpcResult } from "../../RpcListener" import { CT } from "@start9labs/start-sdk" @@ -46,8 +45,11 @@ import { OldConfigSpec, matchOldConfigSpec, transformConfigSpec, + transformNewConfigToOld, transformOldConfigToNew, } from "./transformConfigSpec" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { StorePath } from "@start9labs/start-sdk/cjs/lib/store/PathBuilder" type Optional = A | undefined | null function todo(): never { @@ -57,7 +59,43 @@ const execFile = promisify(childProcess.execFile) 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" +const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as 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( { @@ -101,6 +139,11 @@ const matchSetResult = object( ["depends-on", "dependsOn"], ) +type OldGetConfigRes = { + config?: null | Record + spec: OldConfigSpec +} + export type PackagePropertiesV2 = { [name: string]: PackagePropertyObject | PackagePropertyString } @@ -187,7 +230,7 @@ export class SystemForEmbassy implements System { const moduleCode = await import(EMBASSY_JS_LOCATION) .catch((_) => require(EMBASSY_JS_LOCATION)) .catch(async (_) => { - console.error("Could not load the js") + console.error(utils.asError("Could not load the js")) console.error({ exists: await fs.stat(EMBASSY_JS_LOCATION), }) @@ -199,153 +242,73 @@ export class SystemForEmbassy implements System { moduleCode, ) } + constructor( readonly manifest: Manifest, readonly moduleCode: Partial, ) {} - async execute( - effectCreator: ReturnType, - options: { - id: string - procedure: JsonPath - input: unknown - timeout?: number | undefined - }, - ): Promise { - const effects = effectCreator(options.id) - return this._execute(effects, options) - .then((x) => - matches(x) - .when( - object({ - result: any, - }), - (x) => x, - ) - .when( - object({ - error: string, - }), - (x) => ({ - error: { - code: 0, - message: x.error, - }, - }), - ) - .when( - object({ - "error-code": tuple(number, string), - }), - ({ "error-code": [code, message] }) => ({ - error: { - code, - message, - }, - }), - ) - .defaultTo({ result: x }), - ) - .catch((error: unknown) => { - if (error instanceof Error) - return { - error: { - code: 0, - message: error.name, - data: { - details: error.message, - debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`, - }, - }, - } - if (matchRpcResult.test(error)) return error + + async actionsMetadata(effects: T.Effects): Promise { + const actions = Object.entries(this.manifest.actions ?? {}) + return Promise.all( + actions.map(async ([actionId, action]): Promise => { + const name = action.name ?? actionId + const description = action.description + const warning = action.warning ?? null + const disabled = false + const input = (await convertToNewConfig(action["input-spec"] as any)) + .spec + 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 ? "onlyRunning" : + "onlyStopped" + + const group = null return { - error: { - code: 0, - message: String(error), - }, + name, + description, + warning, + disabled, + allowedStatuses, + group, + input, } - }) + }), + ) } + + async containerInit(): Promise {} + async exit(): Promise { if (this.currentRunning) await this.currentRunning.clean() delete this.currentRunning } - async _execute( - effects: Effects, - options: { - procedure: JsonPath - input: unknown - timeout?: number | undefined - }, - ): Promise { - const input = options.input - switch (options.procedure) { - case "/backup/create": - return this.createBackup(effects, options.timeout || null) - case "/backup/restore": - return this.restoreBackup(effects, options.timeout || null) - case "/config/get": - return this.getConfig(effects, options.timeout || null) - case "/config/set": - return this.setConfig(effects, input, options.timeout || null) - case "/properties": - return this.properties(effects, options.timeout || null) - case "/actions/metadata": - return todo() - case "/init": - return this.init( - effects, - string.optional().unsafeCast(input), - options.timeout || null, - ) - case "/uninit": - return this.uninit( - effects, - string.optional().unsafeCast(input), - options.timeout || null, - ) - case "/main/start": - return this.mainStart(effects, options.timeout || null) - case "/main/stop": - return this.mainStop(effects, options.timeout || null) - default: - const procedures = unNestPath(options.procedure) - switch (true) { - case procedures[1] === "actions" && procedures[3] === "get": - return this.action( - effects, - procedures[2], - input, - options.timeout || null, - ) - case procedures[1] === "actions" && procedures[3] === "run": - return this.action( - effects, - procedures[2], - input, - options.timeout || null, - ) - case procedures[1] === "dependencies" && procedures[3] === "query": - return this.dependenciesAutoconfig( - effects, - procedures[2], - input, - options.timeout || null, - ) - case procedures[1] === "dependencies" && procedures[3] === "update": - return this.dependenciesAutoconfig( - effects, - procedures[2], - input, - options.timeout || null, - ) - } + async start(effects: MainEffects): Promise { + 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"), + }) } - throw new Error(`Could not find the path for ${options.procedure}`) } - private async init( + + async packageInit( effects: Effects, previousVersion: Optional, timeoutMs: number | null, @@ -421,8 +384,11 @@ export class SystemForEmbassy implements System { id: `${id}-${internal}`, description: interfaceValue.description, hasPrimary: false, - disabled: false, - type: "api", + type: + interfaceValue.ui && + (origin.scheme === "http" || origin.sslScheme === "https") + ? "ui" + : "api", masked: false, path: "", schemeOverride: null, @@ -462,7 +428,7 @@ export class SystemForEmbassy implements System { }) } } - private async uninit( + async packageUninit( effects: Effects, nextVersion: Optional, timeoutMs: number | null, @@ -470,49 +436,29 @@ export class SystemForEmbassy implements System { // TODO Do a migration down if the version exists await effects.setMainStatus({ status: "stopped" }) } - private async mainStart( - effects: Effects, - timeoutMs: number | null, - ): Promise { - if (!!this.currentRunning) return - this.currentRunning = new MainLoop(this, effects) - } - private async mainStop( - effects: Effects, - timeoutMs: number | null, - ): Promise { - const { currentRunning } = this - this.currentRunning?.clean() - delete this.currentRunning - if (currentRunning) { - await currentRunning.clean({ - timeout: fromDuration(this.manifest.main["sigterm-timeout"]), - }) - } - const durationValue = duration( - fromDuration(this.manifest.main["sigterm-timeout"]), - "s", - ) - return durationValue - } - private async createBackup( + async createBackup( effects: Effects, timeoutMs: number | null, ): Promise { const backup = this.manifest.backup.create if (backup.type === "docker") { - const container = await DockerProcedureContainer.of(effects, backup, { - ...this.manifest.volumes, - BACKUP: { type: "backup", readonly: false }, - }) + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + backup, + { + ...this.manifest.volumes, + BACKUP: { type: "backup", readonly: false }, + }, + ) await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) } else { const moduleCode = await this.moduleCode await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) } } - private async restoreBackup( + async restoreBackup( effects: Effects, timeoutMs: number | null, ): Promise { @@ -520,6 +466,7 @@ export class SystemForEmbassy implements System { if (restoreBackup.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, restoreBackup, { ...this.manifest.volumes, @@ -535,23 +482,22 @@ export class SystemForEmbassy implements System { await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest)) } } - private async getConfig( + async getConfig( effects: Effects, timeoutMs: number | null, ): Promise { - return this.getConfigUncleaned(effects, timeoutMs) - .then(removePointers) - .then(convertToNewConfig) + return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig) } private async getConfigUncleaned( effects: Effects, timeoutMs: number | null, - ): Promise { + ): Promise { const config = this.manifest.config?.get if (!config) return { spec: {} } if (config.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, config, this.manifest.volumes, ) @@ -570,30 +516,36 @@ export class SystemForEmbassy implements System { 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 x.result + 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 } } - private async setConfig( + async setConfig( effects: Effects, newConfigWithoutPointers: unknown, timeoutMs: number | null, ): Promise { - const newConfig = structuredClone(newConfigWithoutPointers) - await updateConfig( - effects, - this.manifest, - await this.getConfigUncleaned(effects, timeoutMs).then((x) => x.spec), - newConfig, + 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 container = await DockerProcedureContainer.of( effects, + this.manifest.id, setConfigValue, this.manifest.volumes, ) @@ -647,13 +599,13 @@ export class SystemForEmbassy implements System { dependencies: Object.entries(dependsOn).flatMap(([key, value]) => { const dependency = this.manifest.dependencies?.[key] if (!dependency) return [] - const versionSpec = dependency.version + const versionRange = dependency.version const registryUrl = DEFAULT_REGISTRY const kind = "running" return [ { id: key, - versionSpec, + versionRange, registryUrl, kind, healthChecks: [...value], @@ -663,23 +615,29 @@ export class SystemForEmbassy implements System { }) } - private async migration( + async migration( effects: Effects, fromVersion: string, timeoutMs: number | null, ): Promise { - const fromEmver = EmVer.from(fromVersion) - const currentEmver = EmVer.from(this.manifest.version) + 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]) => [EmVer.from(version), procedure] as const) + .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]) => [EmVer.from(version), procedure] as const) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) .find( ([versionEmver, procedure]) => versionEmver.greaterThan(fromEmver) && @@ -696,6 +654,7 @@ export class SystemForEmbassy implements System { if (procedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, procedure, this.manifest.volumes, ) @@ -728,16 +687,17 @@ export class SystemForEmbassy implements System { } return { configured: true } } - private async properties( + async properties( effects: Effects, timeoutMs: number | null, - ): Promise> { + ): 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 container = await DockerProcedureContainer.of( effects, + this.manifest.id, setConfigValue, this.manifest.volumes, ) @@ -758,57 +718,81 @@ export class SystemForEmbassy implements System { if (!method) throw new Error("Expecting that the method properties exists") const properties = matchProperties.unsafeCast( - await method(polyfillEffects(effects, this.manifest)).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]) - }), + await method(polyfillEffects(effects, this.manifest)).then( + fromReturnType, + ), ) return asProperty(properties.data) } throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`) } - private async action( + async action( effects: Effects, actionId: string, formData: unknown, timeoutMs: number | null, ): Promise { const actionProcedure = this.manifest.actions?.[actionId]?.implementation - if (!actionProcedure) return { message: "Action not found", value: null } + const toActionResult = ({ + message, + value = "", + copyable, + qr, + }: U.ActionResult): T.ActionResult => ({ + version: "0", + message, + value, + 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, + { + subcontainer, + }, ) - return JSON.parse( - ( - await container.execFail( - [ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(formData), - ], - timeoutMs, - ) - ).stdout.toString(), + 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( + return await method( polyfillEffects(effects, this.manifest), formData 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 + ) + .then(fromReturnType) + .then(toActionResult) } } - private async dependenciesCheck( + async dependenciesCheck( effects: Effects, id: string, oldConfig: unknown, @@ -819,6 +803,7 @@ export class SystemForEmbassy implements System { if (actionProcedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, actionProcedure, this.manifest.volumes, ) @@ -853,22 +838,22 @@ export class SystemForEmbassy implements System { return {} } } - private async dependenciesAutoconfig( + async dependenciesAutoconfig( effects: Effects, id: string, - oldConfig: unknown, + input: unknown, timeoutMs: number | null, ): Promise { + const oldConfig = object({ remoteConfig: any }).unsafeCast( + input, + ).remoteConfig // TODO: docker const moduleCode = await this.moduleCode const method = moduleCode.dependencies?.[id]?.autoConfigure - if (!method) - throw new Error( - `Expecting that the method dependency autoConfigure ${id} exists`, - ) + if (!method) return return (await method( polyfillEffects(effects, this.manifest), - oldConfig as any, + oldConfig, ).then((x) => { if ("result" in x) return x.result if ("error" in x) throw new Error("Error getting config: " + x.error) @@ -876,14 +861,6 @@ export class SystemForEmbassy implements System { })) as any } } -async function removePointers(value: T.ConfigRes): Promise { - const startingSpec = structuredClone(value.spec) - const config = - value.config && cleanConfigFromPointers(value.config, startingSpec) - const spec = cleanSpecOfPointers(startingSpec) - - return { config, spec } -} const matchPointer = object({ type: literal("pointer"), @@ -943,108 +920,102 @@ type CleanConfigFromPointers = } : null -function cleanConfigFromPointers( - config: C, - spec: S, -): CleanConfigFromPointers { - const newConfig = {} as CleanConfigFromPointers - - if (!(object.test(config) && object.test(spec)) || newConfig == null) - return null as CleanConfigFromPointers - - for (const key of Object.keys(spec)) { - if (!isKeyOf(key, spec)) continue - if (!isKeyOf(key, config)) continue - const partSpec = spec[key] - if (matchPointer.test(partSpec)) continue - ;(newConfig as any)[key] = matchSpec.test(partSpec) - ? cleanConfigFromPointers(config[key], partSpec.spec) - : config[key] - } - return newConfig as CleanConfigFromPointers -} - async function updateConfig( effects: Effects, manifest: Manifest, - spec: unknown, - mutConfigValue: unknown, + spec: OldConfigSpec, + mutConfigValue: Record, ) { - if (!dictionary([string, unknown]).test(spec)) return - if (!dictionary([string, unknown]).test(mutConfigValue)) return for (const key in spec) { const specValue = spec[key] - const newConfigValue = mutConfigValue[key] - if (matchSpec.test(specValue)) { - const updateObject = { spec: newConfigValue } + if (specValue.type === "object") { await updateConfig( effects, manifest, - { spec: specValue.spec }, - updateObject, + specValue.spec as OldConfigSpec, + mutConfigValue[key] as Record, ) - mutConfigValue[key] = updateObject.spec - } - if ( - matchVariants.test(specValue) && - object({ tag: object({ id: string }) }).test(newConfigValue) && - newConfigValue.tag.id in specValue.variants - ) { - // Not going to do anything on the variants... - } - if (!matchPointer.test(specValue)) continue - if (matchPointerConfig.test(specValue)) { - const configValue = (await effects.store.get({ - packageId: specValue["package-id"], - callback() {}, - path: `${EMBASSY_POINTER_PATH_PREFIX}${specValue.selector}` as any, - })) as any - mutConfigValue[key] = configValue - } - if (matchPointerPackage.test(specValue)) { - if (specValue.target === "tor-key") - throw new Error("This service uses an unsupported target TorKey") - - const specInterface = specValue.interface - const serviceInterfaceId = extractServiceInterfaceId( + } 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, - specInterface, + specValue.variants[union[specValue.tag.id] as string] as OldConfigSpec, + mutConfigValue[key] as Record, ) - if (!serviceInterfaceId) { - mutConfigValue[key] = "" - return - } - const filled = await utils - .getServiceInterface(effects, { + } 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"], - id: serviceInterfaceId, + callback: () => effects.restart(), + path: EMBASSY_POINTER_PATH_PREFIX, }) - .once() - .catch((x) => { - console.error("Could not get the service interface", x) - return null - }) - const catchFn = (fn: () => X) => { - try { - return fn() - } catch (e) { - return undefined + 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 } - const url: string = - filled === 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 } } } @@ -1062,7 +1033,9 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { const serviceInterfaceId = `${specInterface}-${internalPort}` return serviceInterfaceId } -async function convertToNewConfig(value: T.ConfigRes): Promise { +async function convertToNewConfig( + value: OldGetConfigRes, +): Promise { const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec) const spec = transformConfigSpec(valueSpec) if (!value.config) return { spec, config: null } 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 index 8ce6cabbc..bd8856b42 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -55,10 +55,13 @@ export const matchManifest = object( string, every( matchProcedure, - object({ - name: string, - ["success-message"]: string, - }), + object( + { + name: string, + ["success-message"]: string, + }, + ["success-message"], + ), ), ]), config: object({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 2b7363cbf..c212722e6 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -3,7 +3,7 @@ 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 } from "@start9labs/start-sdk" +import { daemons, startSdk, T, utils } from "@start9labs/start-sdk" import "isomorphic-fetch" import { Manifest } from "./matchManifest" import { DockerProcedureContainer } from "./DockerProcedureContainer" @@ -124,19 +124,18 @@ export const polyfillEffects = ( wait(): Promise> term(): Promise } { - const dockerProcedureContainer = DockerProcedureContainer.of( + const promiseSubcontainer = DockerProcedureContainer.createSubContainer( effects, + manifest.id, manifest.main, manifest.volumes, ) - const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => + const daemon = promiseSubcontainer.then((subcontainer) => daemons.runCommand()( effects, - { id: manifest.main.image }, + subcontainer, [input.command, ...(input.args || [])], - { - overlay: dockerProcedureContainer.overlay, - }, + {}, ), ) return { @@ -223,16 +222,16 @@ export const polyfillEffects = ( return new Promise((resolve) => setTimeout(resolve, timeMs)) }, trace(whatToPrint: string): void { - console.trace(whatToPrint) + console.trace(utils.asError(whatToPrint)) }, warn(whatToPrint: string): void { - console.warn(whatToPrint) + console.warn(utils.asError(whatToPrint)) }, error(whatToPrint: string): void { - console.error(whatToPrint) + console.error(utils.asError(whatToPrint)) }, debug(whatToPrint: string): void { - console.debug(whatToPrint) + console.debug(utils.asError(whatToPrint)) }, info(whatToPrint: string): void { console.log(false) @@ -356,7 +355,7 @@ export const polyfillEffects = ( }) spawned.stderr.on("data", (data: unknown) => { - console.error(String(data)) + console.error(`polyfill.runAsync`, utils.asError(data)) }) const id = async () => { 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 index cb7809903..5ce601c57 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -12,6 +12,7 @@ import { deferred, every, nill, + literal, } from "ts-matches" export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { @@ -38,7 +39,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { values: oldVal.values.reduce( (obj, curr) => ({ ...obj, - [curr]: oldVal["value-names"][curr], + [curr]: oldVal["value-names"][curr] || curr, }), {}, ), @@ -73,7 +74,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { integer: oldVal.integral, step: null, units: oldVal.units || null, - placeholder: oldVal.placeholder || null, + placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null, } } else if (oldVal.type === "object") { newVal = { @@ -104,12 +105,12 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { : [], minLength: null, maxLength: null, - masked: oldVal.masked, + masked: oldVal.masked || false, generate: null, inputmode: "text", placeholder: oldVal.placeholder || null, } - } else { + } else if (oldVal.type === "union") { newVal = { type: "union", name: oldVal.tag.name, @@ -119,7 +120,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { (obj, [id, spec]) => ({ ...obj, [id]: { - name: oldVal.tag["variant-names"][id], + name: oldVal.tag["variant-names"][id] || id, spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)), }, }), @@ -130,6 +131,10 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { default: oldVal.default, immutable: false, } + } else if (oldVal.type === "pointer") { + return inputSpec + } else { + throw new Error(`unknown spec ${JSON.stringify(oldVal)}`) } return { @@ -175,6 +180,10 @@ export function transformOldConfigToNew( ) } + if (isPointer(val)) { + return obj + } + return { ...obj, [key]: newVal, @@ -201,7 +210,7 @@ export function transformNewConfigToOld( [val.tag.id]: config[key].selection, ...transformNewConfigToOld( matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]), - config[key].unionSelectValue, + config[key].value, ), } } @@ -258,6 +267,31 @@ function getListSpec( {}, ), } + } 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, @@ -276,7 +310,7 @@ function getListSpec( : [], minLength: null, maxLength: null, - masked: oldVal.spec.masked, + masked: oldVal.spec.masked || false, generate: null, inputmode: "text", placeholder: oldVal.spec.placeholder || null, @@ -292,7 +326,7 @@ function getListSpec( spec: transformConfigSpec( matchOldConfigSpec.unsafeCast(oldVal.spec.spec), ), - uniqueBy: oldVal.spec["unique-by"], + uniqueBy: oldVal.spec["unique-by"] || null, displayAs: oldVal.spec["display-as"] || null, }, } @@ -313,6 +347,10 @@ 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" } { @@ -324,11 +362,16 @@ function isStringList( ): 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 (["number", "union"].includes(val.subtype)) { + if (["union"].includes(val.subtype)) { throw new Error("Invalid list subtype. enum, string, and object permitted.") } return val.subtype === "object" @@ -347,11 +390,11 @@ type OldDefaultString = typeof matchOldDefaultString._TYPE export const matchOldValueSpecString = object( { + type: literals("string"), + name: string, masked: boolean, copyable: boolean, - type: literals("string"), nullable: boolean, - name: string, placeholder: string, pattern: string, "pattern-description": string, @@ -361,6 +404,9 @@ export const matchOldValueSpecString = object( warning: string, }, [ + "masked", + "copyable", + "nullable", "placeholder", "pattern", "pattern-description", @@ -382,7 +428,7 @@ export const matchOldValueSpecNumber = object( description: string, warning: string, units: string, - placeholder: string, + placeholder: anyOf(number, string), }, ["default", "description", "warning", "units", "placeholder"], ) @@ -466,7 +512,7 @@ const matchOldListValueSpecObject = object( "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"], + ["display-as", "unique-by"], ) const matchOldListValueSpecString = object( { @@ -476,13 +522,22 @@ const matchOldListValueSpecString = object( "pattern-description": string, placeholder: string, }, - ["pattern", "pattern-description", "placeholder"], + ["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( @@ -515,10 +570,36 @@ const matchOldValueSpecList = every( 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, @@ -527,6 +608,7 @@ export const matchOldValueSpec = anyOf( matchOldValueSpecEnum, matchOldValueSpecList, matchOldValueSpecUnion, + matchOldValueSpecPointer, ) type OldValueSpec = typeof matchOldValueSpec._TYPE diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index e7f09952a..51d91abb5 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -1,194 +1,177 @@ -import { ExecuteResult, System } from "../../Interfaces/System" +import { ExecuteResult, Procedure, System } from "../../Interfaces/System" import { unNestPath } from "../../Models/JsonPath" import matches, { any, number, object, string, tuple } from "ts-matches" -import { hostSystemStartOs } from "../HostSystemStartOs" import { Effects } from "../../Models/Effects" import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" -import { T } from "@start9labs/start-sdk" +import { T, utils } from "@start9labs/start-sdk" +import { Volume } from "../../Models/Volume" import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { CallbackHolder } from "../../Models/CallbackHolder" +import { Optional } from "ts-matches/lib/parsers/interfaces" + export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" + +type RunningMain = { + effects: MainEffects + stop: () => Promise + callbacks: CallbackHolder +} + export class SystemForStartOs implements System { - private onTerm: (() => Promise) | undefined + private runningMain: RunningMain | undefined + static of() { return new SystemForStartOs(require(STARTOS_JS_LOCATION)) } + constructor(readonly abi: T.ABI) {} - async execute( - effectCreator: ReturnType, - options: { - id: string - procedure: - | "/init" - | "/uninit" - | "/main/start" - | "/main/stop" - | "/config/set" - | "/config/get" - | "/backup/create" - | "/backup/restore" - | "/actions/metadata" - | `/actions/${string}/get` - | `/actions/${string}/run` - | `/dependencies/${string}/query` - | `/dependencies/${string}/update` - input: unknown - timeout?: number | undefined - }, - ): Promise { - const effects = effectCreator(options.id) - return this._execute(effects, options) - .then((x) => - matches(x) - .when( - object({ - result: any, - }), - (x) => x, - ) - .when( - object({ - error: string, - }), - (x) => ({ - error: { - code: 0, - message: x.error, - }, - }), - ) - .when( - object({ - "error-code": tuple(number, string), - }), - ({ "error-code": [code, message] }) => ({ - error: { - code, - message, - }, - }), - ) - .defaultTo({ result: x }), - ) - .catch((error: unknown) => { - if (error instanceof Error) - return { - error: { - code: 0, - message: error.name, - data: { - details: error.message, - debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`, - }, - }, - } - if (matchRpcResult.test(error)) return error - return { - error: { - code: 0, - message: String(error), - }, - } - }) + containerInit(): Promise { + throw new Error("Method not implemented.") + } + async packageInit( + effects: Effects, + previousVersion: Optional = null, + timeoutMs: number | null = null, + ): Promise { + return void (await this.abi.init({ effects })) + } + async packageUninit( + effects: Effects, + nextVersion: Optional = null, + timeoutMs: number | null = null, + ): Promise { + return void (await this.abi.uninit({ effects, nextVersion })) + } + async createBackup( + effects: T.Effects, + timeoutMs: number | null, + ): Promise { + return void (await this.abi.createBackup({ + effects, + pathMaker: ((options) => + new Volume(options.volume, options.path).path) as T.PathMaker, + })) + } + async restoreBackup( + effects: T.Effects, + timeoutMs: number | null, + ): Promise { + return void (await this.abi.restoreBackup({ + effects, + pathMaker: ((options) => + new Volume(options.volume, options.path).path) as T.PathMaker, + })) + } + getConfig( + effects: T.Effects, + timeoutMs: number | null, + ): Promise { + return this.abi.getConfig({ effects }) + } + async setConfig( + effects: Effects, + input: { effects: Effects; input: Record }, + timeoutMs: number | null, + ): Promise { + const _: unknown = await this.abi.setConfig({ effects, input }) + return + } + migration( + effects: Effects, + fromVersion: string, + timeoutMs: number | null, + ): Promise { + throw new Error("Method not implemented.") } - async _execute( + properties( effects: Effects, - options: { - procedure: - | "/init" - | "/uninit" - | "/main/start" - | "/main/stop" - | "/config/set" - | "/config/get" - | "/backup/create" - | "/backup/restore" - | "/actions/metadata" - | `/actions/${string}/get` - | `/actions/${string}/run` - | `/dependencies/${string}/query` - | `/dependencies/${string}/update` - input: unknown - timeout?: number | undefined - }, - ): Promise { - switch (options.procedure) { - case "/init": { - const previousVersion = - string.optional().unsafeCast(options.input) || null - return this.abi.init({ effects, previousVersion }) - } - case "/uninit": { - const nextVersion = string.optional().unsafeCast(options.input) || null - return this.abi.uninit({ effects, nextVersion }) - } - case "/main/start": { - if (this.onTerm) await this.onTerm() - const started = async (onTerm: () => Promise) => { - await effects.setMainStatus({ status: "running" }) - this.onTerm = onTerm - } - const daemons = await ( - await this.abi.main({ - effects: { ...effects, _type: "main" }, - started, - }) - ).build() - this.onTerm = daemons.term - return - } - case "/main/stop": { - if (this.onTerm) await this.onTerm() - await effects.setMainStatus({ status: "stopped" }) - delete this.onTerm - return duration(30, "s") - } - case "/config/set": { - const input = options.input as any // TODO - return this.abi.setConfig({ effects, input }) - } - case "/config/get": { - return this.abi.getConfig({ effects }) - } - case "/backup/create": - case "/backup/restore": - throw new Error("this should be called with the init/unit") - case "/actions/metadata": { - return this.abi.actionsMetadata({ effects }) - } - default: - const procedures = unNestPath(options.procedure) - const id = procedures[2] - switch (true) { - case procedures[1] === "actions" && procedures[3] === "get": { - const action = (await this.abi.actions({ effects }))[id] - if (!action) throw new Error(`Action ${id} not found`) - return action.getConfig({ effects }) - } - case procedures[1] === "actions" && procedures[3] === "run": { - const action = (await this.abi.actions({ effects }))[id] - if (!action) throw new Error(`Action ${id} not found`) - return action.run({ effects, input: options.input as any }) // TODO - } - case procedures[1] === "dependencies" && procedures[3] === "query": { - const dependencyConfig = this.abi.dependencyConfig[id] - if (!dependencyConfig) - throw new Error(`dependencyConfig ${id} not found`) - const localConfig = options.input - return dependencyConfig.query({ effects }) - } - case procedures[1] === "dependencies" && procedures[3] === "update": { - const dependencyConfig = this.abi.dependencyConfig[id] - if (!dependencyConfig) - throw new Error(`dependencyConfig ${id} not found`) - return dependencyConfig.update(options.input as any) // TODO - } - } - return + timeoutMs: number | null, + ): Promise { + throw new Error("Method not implemented.") + } + async action( + effects: Effects, + id: string, + formData: unknown, + timeoutMs: number | null, + ): Promise { + const action = (await this.abi.actions({ effects }))[id] + if (!action) throw new Error(`Action ${id} not found`) + return action.run({ effects }) + } + dependenciesCheck( + effects: Effects, + id: string, + oldConfig: unknown, + timeoutMs: number | null, + ): Promise { + const dependencyConfig = this.abi.dependencyConfig[id] + if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`) + return dependencyConfig.query({ effects }) + } + async dependenciesAutoconfig( + effects: Effects, + id: string, + remoteConfig: unknown, + timeoutMs: number | null, + ): Promise { + const dependencyConfig = this.abi.dependencyConfig[id] + if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`) + const queryResults = await this.getConfig(effects, timeoutMs) + return void (await dependencyConfig.update({ + queryResults, + remoteConfig, + })) // TODO + } + async actionsMetadata(effects: T.Effects): Promise { + return this.abi.actionsMetadata({ effects }) + } + + async init(): Promise {} + + async exit(): Promise {} + + async start(effects: MainEffects): Promise { + if (this.runningMain) await this.stop() + let mainOnTerm: () => Promise | undefined + const started = async (onTerm: () => Promise) => { + await effects.setMainStatus({ status: "running" }) + mainOnTerm = onTerm + } + const daemons = await ( + await this.abi.main({ + effects: effects as MainEffects, + started, + }) + ).build() + this.runningMain = { + effects, + stop: async () => { + if (mainOnTerm) await mainOnTerm() + await daemons.term() + }, + callbacks: new CallbackHolder(), } - throw new Error(`Method ${options.procedure} not implemented.`) } - async exit(effects: Effects): Promise { - return void null + + callCallback(callback: number, args: any[]): void { + if (this.runningMain) { + this.runningMain.callbacks + .callCallback(callback, args) + .catch((error) => + console.error(`callback ${callback} failed`, utils.asError(error)), + ) + } else { + console.warn(`callback ${callback} ignored because system is not running`) + } + } + + async stop(): Promise { + if (this.runningMain) { + await this.runningMain.stop() + await this.runningMain.effects.clearCallbacks() + this.runningMain = undefined + } } } diff --git a/container-runtime/src/Interfaces/AllGetDependencies.ts b/container-runtime/src/Interfaces/AllGetDependencies.ts index 88a200900..ca5c43585 100644 --- a/container-runtime/src/Interfaces/AllGetDependencies.ts +++ b/container-runtime/src/Interfaces/AllGetDependencies.ts @@ -1,6 +1,7 @@ import { GetDependency } from "./GetDependency" import { System } from "./System" -import { GetHostSystem, HostSystem } from "./HostSystem" +import { MakeMainEffects, MakeProcedureEffects } from "./MakeEffects" export type AllGetDependencies = GetDependency<"system", Promise> & - GetDependency<"hostSystem", GetHostSystem> + GetDependency<"makeProcedureEffects", MakeProcedureEffects> & + GetDependency<"makeMainEffects", MakeMainEffects> diff --git a/container-runtime/src/Interfaces/HostSystem.ts b/container-runtime/src/Interfaces/HostSystem.ts deleted file mode 100644 index 4ba986e3b..000000000 --- a/container-runtime/src/Interfaces/HostSystem.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { types as T } from "@start9labs/start-sdk" - -import { CallbackHolder } from "../Models/CallbackHolder" -import { Effects } from "../Models/Effects" -export type HostSystem = Effects -export type GetHostSystem = ( - callbackHolder: CallbackHolder, -) => (procedureId: null | string) => Effects diff --git a/container-runtime/src/Interfaces/MakeEffects.ts b/container-runtime/src/Interfaces/MakeEffects.ts new file mode 100644 index 000000000..3b25f8180 --- /dev/null +++ b/container-runtime/src/Interfaces/MakeEffects.ts @@ -0,0 +1,4 @@ +import { Effects } from "../Models/Effects" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +export type MakeProcedureEffects = (procedureId: string) => Effects +export type MakeMainEffects = () => MainEffects diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 986288411..1348b79e9 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -1,33 +1,87 @@ import { types as T } from "@start9labs/start-sdk" -import { JsonPath } from "../Models/JsonPath" import { RpcResult } from "../Adapters/RpcListener" -import { hostSystemStartOs } from "../Adapters/HostSystemStartOs" +import { Effects } from "../Models/Effects" +import { CallbackHolder } from "../Models/CallbackHolder" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { Optional } from "ts-matches/lib/parsers/interfaces" + +export type Procedure = + | "/init" + | "/uninit" + | "/config/set" + | "/config/get" + | "/backup/create" + | "/backup/restore" + | "/actions/metadata" + | "/properties" + | `/actions/${string}/get` + | `/actions/${string}/run` + | `/dependencies/${string}/query` + | `/dependencies/${string}/update` + export type ExecuteResult = | { ok: unknown } | { err: { code: number; message: string } } -export interface System { - // init(effects: Effects): Promise - // exit(effects: Effects): Promise - // start(effects: Effects): Promise - // stop(effects: Effects, options: { timeout: number, signal?: number }): Promise - - execute( - effectCreator: ReturnType, - options: { - id: string - procedure: JsonPath - input: unknown - timeout?: number - }, - ): Promise - // sandbox( - // effects: Effects, - // options: { - // procedure: JsonPath - // input: unknown - // timeout?: number - // }, - // ): Promise - - exit(effects: T.Effects): Promise +export type System = { + containerInit(): Promise + + start(effects: MainEffects): Promise + callCallback(callback: number, args: any[]): void + stop(): Promise + + packageInit( + effects: Effects, + previousVersion: Optional, + 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 + getConfig(effects: T.Effects, timeoutMs: number | null): Promise + setConfig( + effects: Effects, + input: { effects: Effects; input: Record }, + timeoutMs: number | null, + ): Promise + migration( + effects: Effects, + fromVersion: string, + timeoutMs: number | null, + ): Promise + properties( + effects: Effects, + timeoutMs: number | null, + ): Promise + action( + effects: Effects, + actionId: string, + formData: unknown, + timeoutMs: number | null, + ): Promise + + dependenciesCheck( + effects: Effects, + id: string, + oldConfig: unknown, + timeoutMs: number | null, + ): Promise + dependenciesAutoconfig( + effects: Effects, + id: string, + oldConfig: unknown, + timeoutMs: number | null, + ): Promise + actionsMetadata(effects: T.Effects): 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 index 6539dda88..b51af0bee 100644 --- a/container-runtime/src/Models/CallbackHolder.ts +++ b/container-runtime/src/Models/CallbackHolder.ts @@ -1,12 +1,14 @@ export class CallbackHolder { constructor() {} - private root = (Math.random() + 1).toString(36).substring(7) private inc = 0 private callbacks = new Map() private newId() { return this.inc++ } - addCallback(callback: Function) { + addCallback(callback?: Function) { + if (!callback) { + return + } const id = this.newId() this.callbacks.set(id, callback) return id diff --git a/container-runtime/src/Models/JsonPath.ts b/container-runtime/src/Models/JsonPath.ts index 314019154..95a2b3a00 100644 --- a/container-runtime/src/Models/JsonPath.ts +++ b/container-runtime/src/Models/JsonPath.ts @@ -28,8 +28,6 @@ export const jsonPath = some( literals( "/init", "/uninit", - "/main/start", - "/main/stop", "/config/set", "/config/get", "/backup/create", diff --git a/container-runtime/src/index.ts b/container-runtime/src/index.ts index 74be5b73a..5454bee3d 100644 --- a/container-runtime/src/index.ts +++ b/container-runtime/src/index.ts @@ -1,12 +1,13 @@ import { RpcListener } from "./Adapters/RpcListener" import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" -import { hostSystemStartOs } from "./Adapters/HostSystemStartOs" +import { makeMainEffects, makeProcedureEffects } from "./Adapters/EffectCreator" import { AllGetDependencies } from "./Interfaces/AllGetDependencies" import { getSystem } from "./Adapters/Systems" const getDependencies: AllGetDependencies = { system: getSystem, - hostSystem: () => hostSystemStartOs, + makeProcedureEffects: () => makeProcedureEffects, + makeMainEffects: () => makeMainEffects, } new RpcListener(getDependencies) diff --git a/container-runtime/tsconfig.json b/container-runtime/tsconfig.json index 0b2fe6e32..6981133d6 100644 --- a/container-runtime/tsconfig.json +++ b/container-runtime/tsconfig.json @@ -13,9 +13,10 @@ "declaration": true, "noImplicitAny": true, "esModuleInterop": true, - "types": ["node"], + "types": ["node", "jest"], "moduleResolution": "Node16", - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true }, "ts-node": { "compilerOptions": { diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index 1c571408a..61429821c 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -8,7 +8,11 @@ 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 -sudo mount debian.${ARCH}.squashfs tmp/lower +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= diff --git a/core/Cargo.lock b/core/Cargo.lock index 9f7face93..e9b9330e3 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -122,33 +122,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -162,9 +162,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "brotli", "flate2", @@ -231,18 +231,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -268,9 +268,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" dependencies = [ "bindgen", "cc", @@ -306,7 +306,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "itoa", "matchit", "memchr", @@ -333,9 +333,9 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "itoa", "matchit", @@ -385,7 +385,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -405,9 +405,9 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "pin-project-lite", "tokio", @@ -471,9 +471,9 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base32" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -532,8 +532,8 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.68", - "which", + "syn 2.0.74", + "which 4.4.2", ] [[package]] @@ -609,9 +609,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210" dependencies = [ "arrayref", "arrayvec 0.7.4", @@ -619,7 +619,7 @@ dependencies = [ "cfg-if", "constant_time_eq", "memmap2", - "rayon", + "rayon-core", ] [[package]] @@ -675,9 +675,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cache-padded" @@ -687,13 +687,13 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.0.101" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -729,7 +729,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -800,9 +800,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.7" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" dependencies = [ "clap_builder", "clap_derive", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -822,27 +822,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] @@ -876,9 +876,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "concurrent-queue" @@ -957,18 +957,18 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" dependencies = [ "proc-macro2", "quote", @@ -1036,15 +1036,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -1118,16 +1118,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", "mio", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -1237,14 +1237,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1252,27 +1252,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1303,7 +1303,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1320,13 +1320,13 @@ dependencies = [ [[package]] name = "der_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1349,7 +1349,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1417,9 +1417,9 @@ dependencies = [ [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" @@ -1571,7 +1571,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1580,6 +1580,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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" version = "0.3.9" @@ -1590,6 +1601,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -1668,14 +1689,14 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] [[package]] @@ -1686,9 +1707,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -1829,7 +1850,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -1942,7 +1963,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", @@ -1961,7 +1982,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.2.6", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", @@ -2069,6 +2090,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -2077,9 +2104,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hifijson" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c" +checksum = "9958ab3ce3170c061a27679916bd9b969eceeb5e8b120438e6751d0987655c42" [[package]] name = "hkdf" @@ -2143,9 +2170,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -2160,7 +2187,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2184,9 +2211,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -2208,16 +2235,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -2235,9 +2262,9 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", - "rustls 0.23.10", + "rustls 0.23.12", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2250,7 +2277,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.30", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2264,7 +2291,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -2274,16 +2301,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -2433,9 +2460,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -2476,9 +2503,9 @@ dependencies = [ [[package]] name = "integer-encoding" -version = "4.0.0" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924df4f0e24e2e7f9cdd90babb0b96f93b20f3ecfa949ea9e6613756b8c8e1bf" +checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" dependencies = [ "async-trait", "tokio", @@ -2505,20 +2532,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "isocountry" @@ -2621,21 +2648,21 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "josekit" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0953340cf63354cec4a385f1fbcb3f409a5823778cae236078892f6030ed4565" +checksum = "54b85e2125819afc4fd2ae57416207e792c7e12797858e5db2a6c6f24a166829" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", "flate2", "once_cell", "openssl", @@ -2648,9 +2675,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2778,12 +2805,12 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2800,6 +2827,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.3", ] [[package]] @@ -2837,9 +2865,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lzma-sys" @@ -2946,14 +2974,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3018,14 +3047,26 @@ 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", ] +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.24.3" @@ -3208,29 +3249,29 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_enum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3262,9 +3303,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssh-keys" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9939566c441a6542e87c74310d4ac713c17679c76f296932e732f405bc25e3d" +checksum = "abb830a82898b2ac17c9620ddce839ac3b34b9cb8a1a037cbdbfb9841c756c3e" dependencies = [ "base64 0.21.7", "byteorder", @@ -3275,9 +3316,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3296,7 +3337,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3316,9 +3357,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -3395,9 +3436,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -3474,9 +3515,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", "thiserror", @@ -3485,9 +3526,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" dependencies = [ "pest", "pest_generator", @@ -3495,22 +3536,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" dependencies = [ "once_cell", "pest", @@ -3524,7 +3565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.4.0", ] [[package]] @@ -3559,7 +3600,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3603,9 +3644,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "powerfmt" @@ -3615,9 +3656,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" @@ -3632,7 +3676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3676,6 +3720,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "flate2", + "hex", + "lazy_static", + "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.6.0", + "chrono", + "hex", +] + [[package]] name = "proptest" version = "1.5.0" @@ -3704,7 +3774,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3727,7 +3797,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -3871,16 +3941,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - [[package]] name = "rayon-core" version = "1.12.1" @@ -3917,9 +3977,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] @@ -3937,9 +3997,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -3994,9 +4054,9 @@ dependencies = [ "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4008,7 +4068,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.1.3", "serde", "serde_json", "serde_urlencoded", @@ -4078,7 +4138,7 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.2.3" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#f608480034942f1f521ab95949ab33fbc51d99a9" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#60a974a29c5e6380f7bbfbc1b4716f6d2b20b189" dependencies = [ "async-stream", "async-trait", @@ -4173,7 +4233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.6.0", - "errno", + "errno 0.3.9", "libc", "linux-raw-sys", "windows-sys 0.52.0", @@ -4192,15 +4252,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -4216,9 +4276,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -4226,9 +4286,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -4242,9 +4302,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "aws-lc-rs", "ring", @@ -4272,9 +4332,9 @@ dependencies = [ [[package]] name = "rustyline-async" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6eb06391513b2184f0a5405c11a4a0a5302e8be442f4c5c35267187c2b37d5" +checksum = "bc9396d834c31f9fddd716e7c279e7cb70207092a1e59767918610f5c560c6eb" dependencies = [ "crossterm", "futures-channel", @@ -4342,9 +4402,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -4355,9 +4415,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -4374,9 +4434,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] @@ -4400,23 +4460,24 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", + "memchr", "ryu", "serde", ] @@ -4433,9 +4494,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -4454,15 +4515,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_derive", "serde_json", @@ -4472,14 +4533,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -4488,7 +4549,7 @@ version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ce6afeda22f0b55dde2c34897bce76a629587348480384231205c14b59a01f" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", "libyml", "log", @@ -4577,9 +4638,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -4643,6 +4704,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -4717,7 +4784,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap 2.4.0", "log", "memchr", "once_cell", @@ -4885,9 +4952,9 @@ dependencies = [ [[package]] name = "sscanf" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c713ebd15ce561dd4a13ed62bc2a0368e16806fc30dcaf66ecf1256b2a3fdde6" +checksum = "a147d3cf7e723671ed11355b5b008c8019195f7fc902e213f5557d931e9f839d" dependencies = [ "const_format", "lazy_static", @@ -4897,16 +4964,16 @@ dependencies = [ [[package]] name = "sscanf_macro" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84955aa74a157e5834d58a07be11af7f0ab923f0194a0bb2ea6b3db8b5d1611d" +checksum = "af3a37bdf8e90e77cc60f74473edf28d922ae2eacdd595e67724ccd2381774cc" dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.68", + "syn 2.0.74", "unicode-width", ] @@ -4954,7 +5021,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.5-rev.2" +version = "0.3.6-alpha.5" dependencies = [ "aes", "async-compression", @@ -4964,7 +5031,7 @@ dependencies = [ "axum-server", "backhand", "barrage", - "base32 0.5.0", + "base32 0.5.1", "base64 0.22.1", "base64ct", "basic-cookies", @@ -4986,6 +5053,7 @@ dependencies = [ "ed25519-dalek 2.1.1", "exver", "fd-lock-rs", + "form_urlencoded", "futures", "gpt", "helpers", @@ -4998,7 +5066,7 @@ dependencies = [ "imbl", "imbl-value", "include_dir", - "indexmap 2.2.6", + "indexmap 2.4.0", "indicatif", "integer-encoding", "ipnet", @@ -5020,6 +5088,7 @@ dependencies = [ "nix 0.29.0", "nom 7.1.3", "num", + "num_cpus", "num_enum", "once_cell", "openssh-keys", @@ -5030,6 +5099,7 @@ dependencies = [ "pin-project", "pkcs8", "prettytable-rs", + "procfs", "proptest", "proptest-derive", "rand 0.8.5", @@ -5048,12 +5118,14 @@ dependencies = [ "serde_yml", "sha2 0.10.8", "shell-words", + "signal-hook", "simple-logging", "socket2", "sqlx", "sscanf", "ssh-key", "tar", + "textwrap", "thiserror", "tokio", "tokio-rustls", @@ -5062,7 +5134,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite 0.23.1", "tokio-util", - "toml 0.8.14", + "toml 0.8.19", "torut", "tower-service", "tracing", @@ -5072,10 +5144,14 @@ dependencies = [ "tracing-subscriber", "trust-dns-server", "ts-rs", + "tty-spawn", "typed-builder", + "unix-named-pipe", + "unshare", "url", "urlencoding", "uuid", + "which 6.0.3", "zeroize", ] @@ -5140,9 +5216,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -5201,14 +5277,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5224,13 +5301,24 @@ 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 = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thingbuf" version = "0.1.6" @@ -5243,22 +5331,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -5324,9 +5412,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -5339,22 +5427,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5369,13 +5456,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -5394,16 +5481,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.12", "rustls-pki-types", "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", @@ -5490,21 +5577,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.20", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -5515,7 +5602,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", @@ -5528,22 +5615,22 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.18", ] [[package]] @@ -5560,7 +5647,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -5614,15 +5701,15 @@ dependencies = [ [[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" @@ -5644,7 +5731,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -5796,10 +5883,21 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", "termcolor", ] +[[package]] +name = "tty-spawn" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb91489cf2611235ae8d755d66ab028437980ee573e2230c05af41b136236ad1" +dependencies = [ + "anyhow", + "nix 0.29.0", + "signal-hook", +] + [[package]] name = "tungstenite" version = "0.21.0" @@ -5856,7 +5954,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -5898,6 +5996,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" @@ -5921,9 +6025,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -5937,6 +6041,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unix-named-pipe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad653da8f36ac5825ba06642b5a3cce14a4e52c6a5fab4a8928d53f4426dae2" +dependencies = [ + "errno 0.2.8", + "libc", +] + +[[package]] +name = "unshare" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceda295552a1eda89f8a748237654ad76b9c87e383fc07af5c4e423eb8e7b9b" +dependencies = [ + "libc", + "nix 0.20.0", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5975,9 +6099,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", ] @@ -5996,9 +6120,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[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" @@ -6048,34 +6172,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -6085,9 +6210,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6095,22 +6220,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-streams" @@ -6127,9 +6252,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -6153,6 +6278,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "whoami" version = "1.5.1" @@ -6181,11 +6318,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6200,7 +6337,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6218,7 +6355,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -6238,18 +6384,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "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.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "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]] @@ -6260,9 +6406,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6272,9 +6418,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6284,15 +6430,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6302,9 +6448,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6314,9 +6460,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6326,9 +6472,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6338,9 +6484,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -6353,9 +6499,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -6370,6 +6516,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wyz" version = "0.2.0" @@ -6440,22 +6592,23 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] @@ -6475,32 +6628,32 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.74", ] [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.11+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/core/build-startos-bins.sh b/core/build-containerbox.sh similarity index 54% rename from core/build-startos-bins.sh rename to core/build-containerbox.sh index f81ddb093..e81efcc97 100755 --- a/core/build-startos-bins.sh +++ b/core/build-containerbox.sh @@ -2,13 +2,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")" -set -e +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" @@ -24,22 +28,9 @@ 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' -set +e -fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! 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)"; then - fail=true -fi -if ! 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)"; then - fail=true -fi -set -e -cd core - -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - -if [ -n "$fail" ]; then - exit 1 -fi +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-registrybox.sh b/core/build-registrybox.sh index 9928b6714..3659b372a 100755 --- a/core/build-registrybox.sh +++ b/core/build-registrybox.sh @@ -2,13 +2,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")" -set -e +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" @@ -24,19 +28,9 @@ 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' -set +e -fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! 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)"; then - fail=true -fi -set -e -cd core - -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - -if [ -n "$fail" ]; then - exit 1 +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/helpers/src/lib.rs b/core/helpers/src/lib.rs index d913aefee..80631fea2 100644 --- a/core/helpers/src/lib.rs +++ b/core/helpers/src/lib.rs @@ -50,7 +50,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 { diff --git a/core/install-cli.sh b/core/install-cli.sh index 620600d92..b278947a3 100755 --- a/core/install-cli.sh +++ b/core/install-cli.sh @@ -2,14 +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 + +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/models/src/errors.rs b/core/models/src/errors.rs index 8bbc705ee..ee6b0ae12 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -490,6 +490,7 @@ where { fn with_kind(self, kind: ErrorKind) -> Result; fn with_ctx (ErrorKind, D), D: Display>(self, f: F) -> Result; + fn log_err(self) -> Option; } impl ResultExt for Result where @@ -516,6 +517,18 @@ 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 { @@ -539,6 +552,17 @@ impl ResultExt for Result { } }) } + + 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/package.rs b/core/models/src/id/package.rs index d2665e59a..6e22b9d51 100644 --- a/core/models/src/id/package.rs +++ b/core/models/src/id/package.rs @@ -61,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/procedure_name.rs b/core/models/src/procedure_name.rs index c8ae8c3a8..466835818 100644 --- a/core/models/src/procedure_name.rs +++ b/core/models/src/procedure_name.rs @@ -4,8 +4,6 @@ use crate::{ActionId, PackageId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProcedureName { - StartMain, - StopMain, GetConfig, SetConfig, CreateBackup, @@ -25,8 +23,6 @@ impl ProcedureName { match self { ProcedureName::Init => "/init".to_string(), ProcedureName::Uninit => "/uninit".to_string(), - ProcedureName::StartMain => "/main/start".to_string(), - ProcedureName::StopMain => "/main/stop".to_string(), ProcedureName::SetConfig => "/config/set".to_string(), ProcedureName::GetConfig => "/config/get".to_string(), ProcedureName::CreateBackup => "/backup/create".to_string(), 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/startos/Cargo.toml b/core/startos/Cargo.toml index 842ef44ef..9b3e7a048 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.2" +version = "0.3.6-alpha.5" license = "MIT" [lib] @@ -39,10 +39,10 @@ path = "src/main.rs" [features] cli = [] -container-runtime = [] +container-runtime = ["procfs", "unshare", "tty-spawn"] daemon = [] registry = [] -default = ["cli", "daemon"] +default = ["cli", "daemon", "registry", "container-runtime"] dev = [] unstable = ["console-subscriber", "tokio/tracing"] docker = [] @@ -90,6 +90,7 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", "serde", ] } fd-lock-rs = "0.1.4" +form_urlencoded = "1.2.1" futures = "0.3.28" gpt = "3.1.0" helpers = { path = "../helpers" } @@ -97,7 +98,12 @@ hex = "0.4.3" hmac = "0.12.1" http = "1.0.0" http-body-util = "0.1" -hyper-util = { version = "0.1.5", features = ["tokio", "service"] } +hyper-util = { version = "0.1.5", features = [ + "tokio", + "service", + "http1", + "http2", +] } id-pool = { version = "0.2.2", default-features = false, features = [ "serde", "u16", @@ -124,10 +130,18 @@ log = "0.4.20" mbrman = "0.5.2" models = { version = "*", path = "../models" } new_mime_guess = "4" -nix = { version = "0.29.0", features = ["user", "process", "signal", "fs"] } +nix = { version = "0.29.0", features = [ + "fs", + "mount", + "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"] } @@ -139,6 +153,7 @@ 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.5.0" rand = { version = "0.8.5", features = ["std"] } @@ -159,6 +174,7 @@ serde_with = { version = "3.4.0", features = ["macros", "json"] } 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 = [ @@ -170,7 +186,8 @@ sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } tar = "0.4.40" thiserror = "1.0.49" -tokio = { version = "1.38.0", features = ["full"] } +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"] } @@ -188,7 +205,11 @@ tracing-journald = "0.3.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } trust-dns-server = "0.23.1" 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" +which = "6.0.3" +unix-named-pipe = "0.2.0" +unshare = { version = "0.7.0", optional = true } url = { version = "2.4.1", features = ["serde"] } urlencoding = "2.1.3" uuid = { version = "1.4.1", features = ["v4"] } diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index e93af4a4d..7c4492adc 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -58,6 +58,7 @@ pub struct ActionParams { pub action_id: ActionId, #[command(flatten)] #[ts(type = "{ [key: string]: any } | null")] + #[serde(default)] pub input: StdinDeserializable>, } // impl C diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index d33320b78..d998e9897 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -185,6 +185,8 @@ pub struct LoginParams { #[serde(rename = "__auth_userAgent")] // from Auth middleware user_agent: Option, #[serde(default)] + ephemeral: bool, + #[serde(default)] #[ts(type = "any")] metadata: Value, } @@ -195,28 +197,46 @@ pub async fn login_impl( LoginParams { password, user_agent, + ephemeral, metadata, }: LoginParams, ) -> Result { let password = password.unwrap_or_default().decrypt(&ctx)?; - 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 { + 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()) - }) - .await + ) + }); + 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)] @@ -319,6 +339,7 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { pub struct ListParams { #[arg(skip)] #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware session: InternedString, } @@ -328,9 +349,15 @@ pub async fn list( 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_token(session).hashed().clone(), - sessions: ctx.db.peek().await.into_private().into_sessions().de()?, + current: session, + sessions, }) } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index a52c97f9b..b4419e88e 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -260,7 +260,7 @@ async fn perform_backup( for id in package_ids { if let Some(service) = &*ctx.services.get(id).await { let backup_result = service - .backup(backup_guard.package_backup(id)) + .backup(backup_guard.package_backup(id).await?) .await .err() .map(|e| e.to_string()); diff --git a/core/startos/src/backup/os.rs b/core/startos/src/backup/os.rs index 7c8119e79..6f08c5f43 100644 --- a/core/startos/src/backup/os.rs +++ b/core/startos/src/backup/os.rs @@ -1,3 +1,4 @@ +use imbl_value::InternedString; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use patch_db::Value; @@ -97,7 +98,7 @@ impl OsBackupV0 { #[serde(rename = "kebab-case")] struct OsBackupV1 { server_id: String, // uuidv4 - hostname: String, // embassy-- + 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 @@ -127,7 +128,7 @@ impl OsBackupV1 { struct OsBackupV2 { server_id: String, // uuidv4 - hostname: String, // - + 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 diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 23e0c8ac1..28d70653f 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -158,7 +158,7 @@ async fn restore_packages( let backup_guard = Arc::new(backup_guard); let mut tasks = BTreeMap::new(); for id in ids { - let backup_dir = backup_guard.clone().package_backup(&id); + 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 diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index f3b6b5f5c..032f70848 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -8,6 +8,7 @@ use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; use exver::Version; +use imbl_value::InternedString; use models::PackageId; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -213,7 +214,7 @@ pub struct BackupInfo { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { - pub title: String, + pub title: InternedString, pub version: VersionString, pub os_version: Version, pub timestamp: DateTime, diff --git a/core/startos/src/bins/container_cli.rs b/core/startos/src/bins/container_cli.rs index a33a99131..db7cbd36a 100644 --- a/core/startos/src/bins/container_cli.rs +++ b/core/startos/src/bins/container_cli.rs @@ -15,7 +15,7 @@ pub fn main(args: impl IntoIterator) { EmbassyLogger::init(); if let Err(e) = CliApp::new( |cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)), - crate::service::service_effect_handler::service_effect_handler(), + crate::service::effects::handler(), ) .run(args) { diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index f4aa411b5..394d42c8d 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -141,6 +141,7 @@ async fn setup_or_init( } else { let init_ctx = InitContext::init(config).await?; let handle = init_ctx.progress.clone(); + let err_channel = init_ctx.error.clone(); let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); let init_phases = InitPhases::new(&handle); @@ -148,47 +149,55 @@ async fn setup_or_init( server.serve_init(init_ctx); - 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 + 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, + config.datadir(), + 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?; - let disk_guid = Arc::new(String::from(guid_string.trim())); - let requires_reboot = crate::disk::main::import( - &**disk_guid, - config.datadir(), 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))?; - } - disk_phase.complete(); - tracing::info!("Loaded Disk"); - - if requires_reboot.0 { - let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); - reboot_phase.start(); - return Ok(Err(Shutdown { - export_args: Some((disk_guid, config.datadir().to_owned())), - restart: true, - })); - } + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + } + disk_phase.complete(); + tracing::info!("Loaded Disk"); + + if requires_reboot.0 { + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: Some((disk_guid, config.datadir().to_owned())), + restart: true, + })); + } - let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; + let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; - let rpc_ctx = RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + let rpc_ctx = + RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; - Ok(Ok((rpc_ctx, handle))) + Ok::<_, Error>(Ok((rpc_ctx, handle))) + } + .await + .map_err(|e| { + err_channel.send_replace(Some(e.clone_output())); + e + }) } } diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 7576c41e9..d383f3091 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::ffi::OsString; use std::net::{Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -136,6 +137,7 @@ pub fn main(args: impl IntoIterator) { 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"); diff --git a/core/startos/src/config/mod.rs b/core/startos/src/config/mod.rs index 01309a16f..22edd98f7 100644 --- a/core/startos/src/config/mod.rs +++ b/core/startos/src/config/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::sync::Arc; use std::time::Duration; @@ -178,13 +179,68 @@ pub struct SetParams { // )] #[instrument(skip_all)] pub fn set() -> ParentHandler { - ParentHandler::new().root_handler( - from_fn_async(set_impl) - .with_metadata("sync_db", Value::Bool(true)) - .with_inherited(|set_params, id| (id, set_params)) - .no_display() - .with_call_remote::(), - ) + ParentHandler::new() + .root_handler( + from_fn_async(set_impl) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|set_params, id| (id, set_params)) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "dry", + from_fn_async(set_dry) + .with_inherited(|set_params, id| (id, set_params)) + .no_display() + .with_call_remote::(), + ) +} + +pub async fn set_dry( + ctx: RpcContext, + _: Empty, + ( + id, + SetParams { + timeout, + config: StdinDeserializable(config), + }, + ): (PackageId, SetParams), +) -> Result, Error> { + let mut breakages = BTreeSet::new(); + + let procedure_id = Guid::new(); + + let db = ctx.db.peek().await; + for dep in db + .as_public() + .as_package_data() + .as_entries()? + .into_iter() + .filter_map( + |(k, v)| match v.as_current_dependencies().contains_key(&id) { + Ok(true) => Some(Ok(k)), + Ok(false) => None, + Err(e) => Some(Err(e)), + }, + ) + { + let dep_id = dep?; + + let Some(dependent) = &*ctx.services.get(&dep_id).await else { + continue; + }; + + if dependent + .dependency_config(procedure_id.clone(), id.clone(), config.clone()) + .await? + .is_some() + { + breakages.insert(dep_id); + } + } + + Ok(breakages) } #[derive(Default)] diff --git a/core/startos/src/context/init.rs b/core/startos/src/context/init.rs index f5f4a5430..566457a9c 100644 --- a/core/startos/src/context/init.rs +++ b/core/startos/src/context/init.rs @@ -3,6 +3,7 @@ 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; @@ -12,6 +13,7 @@ use crate::Error; pub struct InitContextSeed { pub config: ServerConfig, + pub error: watch::Sender>, pub progress: FullProgressTracker, pub shutdown: Sender<()>, pub rpc_continuations: RpcContinuations, @@ -25,6 +27,7 @@ impl InitContext { 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(), diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 4920a6223..5330c58bc 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::future::Future; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::ops::Deref; use std::path::PathBuf; @@ -6,17 +7,20 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; +use chrono::{TimeDelta, Utc}; +use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use josekit::jwk::Jwk; use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{CallRemote, Context, Empty}; -use tokio::sync::{broadcast, Mutex, RwLock}; +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::auth::Sessions; use crate::context::config::ServerConfig; use crate::db::model::Database; use crate::dependencies::compute_dependency_config_errs; @@ -28,11 +32,13 @@ use crate::net::utils::{find_eth_iface, find_wifi_iface}; use crate::net::wifi::WpaCli; use crate::prelude::*; use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle}; -use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; +use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations}; +use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; use crate::util::lshw::{lshw, LshwDevice}; +use crate::util::sync::SyncMutex; pub struct RpcContextSeed { is_closed: AtomicBool, @@ -41,7 +47,9 @@ pub struct RpcContextSeed { pub ethernet_interface: String, pub datadir: PathBuf, pub disk_guid: Arc, + pub ephemeral_sessions: SyncMutex, pub db: TypedPatchDb, + pub sync_db: watch::Sender, pub account: RwLock, pub net_controller: Arc, pub s9pk_arch: Option<&'static str>, @@ -52,11 +60,13 @@ pub struct RpcContextSeed { 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, } @@ -88,12 +98,14 @@ impl InitRpcContextPhases { } pub struct CleanupInitPhases { + cleanup_sessions: PhaseProgressTrackerHandle, init_services: PhaseProgressTrackerHandle, check_dependencies: 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_dependencies: handle.add_phase("Checking dependencies".into(), Some(1)), } @@ -168,6 +180,8 @@ impl RpcContext { let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024; read_device_info.complete(); + let crons = SyncMutex::new(BTreeMap::new()); + if !db .peek() .await @@ -177,18 +191,24 @@ impl RpcContext { .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_public_mut() - .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(), + ) }); } @@ -210,6 +230,8 @@ impl RpcContext { find_eth_iface().await? }, disk_guid, + ephemeral_sessions: SyncMutex::new(Sessions::new()), + sync_db: watch::Sender::new(db.sequence().await), db, account: RwLock::new(account), net_controller, @@ -225,6 +247,7 @@ impl RpcContext { 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)))), @@ -250,6 +273,7 @@ impl RpcContext { .with_kind(crate::ErrorKind::ParseUrl)?, hardware: Hardware { devices, ram }, start_time: Instant::now(), + crons, #[cfg(feature = "dev")] dev: Dev { lxc: Mutex::new(BTreeMap::new()), @@ -264,6 +288,7 @@ impl RpcContext { #[instrument(skip_all)] pub async fn shutdown(self) -> Result<(), Error> { + 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"); @@ -271,14 +296,75 @@ impl RpcContext { Ok(()) } + 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_dependencies, }: CleanupInitPhases, ) -> Result<(), Error> { + cleanup_sessions.start(); + self.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(()) + }) + .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(()) + }) + .await + { + tracing::error!("Error in session cleanup cron: {e}"); + tracing::debug!("{e:?}"); + } + } + }); + cleanup_sessions.complete(); + self.services.init(&self, init_services).await?; tracing::info!("Initialized Package Managers"); @@ -288,7 +374,9 @@ impl RpcContext { for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { let package = package.clone(); let mut current_dependencies = package.as_current_dependencies().de()?; - compute_dependency_config_errs(self, &package_id, &mut current_dependencies).await?; + compute_dependency_config_errs(self, &package_id, &mut current_dependencies) + .await + .log_err(); updated_current_dependents.insert(package_id.clone(), current_dependencies); } self.db diff --git a/core/startos/src/context/setup.rs b/core/startos/src/context/setup.rs index 6041f49b9..999154977 100644 --- a/core/startos/src/context/setup.rs +++ b/core/startos/src/context/setup.rs @@ -5,6 +5,7 @@ use std::time::Duration; use futures::{Future, StreamExt}; use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; use josekit::jwk::Jwk; use patch_db::PatchDb; use rpc_toolkit::Context; @@ -40,7 +41,8 @@ lazy_static::lazy_static! { #[ts(export)] pub struct SetupResult { pub tor_address: String, - pub lan_address: String, + #[ts(type = "string")] + pub lan_address: InternedString, pub root_ca: String, } impl TryFrom<&AccountInfo> for SetupResult { @@ -98,18 +100,6 @@ impl SetupContext { .with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?; 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 diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index e59161e9b..ef35bd30d 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -10,10 +10,12 @@ use clap::Parser; use imbl_value::InternedString; use itertools::Itertools; use patch_db::json_ptr::{JsonPointer, ROOT}; -use patch_db::{Dump, Revision}; +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 tokio::sync::mpsc::{self, UnboundedReceiver}; +use tokio::sync::watch; use tracing::instrument; use ts_rs::TS; @@ -124,14 +126,56 @@ pub struct SubscribeRes { pub guid: Guid, } +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() }) + } + } + } + } + } + } + } +} + pub async fn subscribe( ctx: RpcContext, SubscribeParams { pointer, session }: SubscribeParams, ) -> Result { - let (dump, mut sub) = ctx + 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( diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 22d6440bb..957e42c54 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -3,7 +3,9 @@ use std::collections::{BTreeMap, BTreeSet}; use chrono::{DateTime, Utc}; use exver::VersionRange; use imbl_value::InternedString; -use models::{ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId}; +use models::{ + ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId, VersionString, +}; use patch_db::json_ptr::JsonPointer; use patch_db::HasModel; use reqwest::Url; @@ -63,6 +65,18 @@ impl PackageState { )), } } + 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 }) @@ -323,6 +337,7 @@ pub struct ActionMetadata { #[ts(export)] pub struct PackageDataEntry { pub state_info: PackageState, + pub data_version: Option, pub status: Status, #[ts(type = "string | null")] pub registry: Option, @@ -372,14 +387,13 @@ impl Map for CurrentDependencies { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct CurrentDependencyInfo { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, #[serde(flatten)] pub kind: CurrentDependencyKind, - pub title: String, - pub icon: DataUrl<'static>, - #[ts(type = "string")] - pub registry_url: Url, #[ts(type = "string")] - pub version_spec: VersionRange, + pub version_range: VersionRange, pub config_satisfied: bool, } diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 5f8dc029a..b20693a90 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -20,6 +20,7 @@ use crate::db::model::package::AllPackageData; use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::prelude::*; use crate::progress::FullProgress; +use crate::system::SmtpValue; use crate::util::cpupower::Governor; use crate::version::{Current, VersionT}; use crate::{ARCH, PLATFORM}; @@ -31,7 +32,7 @@ use crate::{ARCH, PLATFORM}; pub struct Public { pub server_info: ServerInfo, pub package_data: AllPackageData, - #[ts(type = "any")] + #[ts(type = "unknown")] pub ui: Value, } impl Public { @@ -107,7 +108,8 @@ pub struct ServerInfo { #[ts(type = "string")] pub platform: InternedString, pub id: String, - pub hostname: String, + #[ts(type = "string")] + pub hostname: InternedString, #[ts(type = "string")] pub version: Version, #[ts(type = "string | null")] @@ -135,7 +137,7 @@ pub struct ServerInfo { #[serde(default)] pub zram: bool, pub governor: Option, - pub smtp: Option, + pub smtp: Option, } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index f6ccc53ad..013648980 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::time::Duration; use clap::Parser; +use imbl_value::InternedString; use models::PackageId; use patch_db::json_patch::merge; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; @@ -10,10 +11,12 @@ use tracing::instrument; use ts_rs::TS; use crate::config::{Config, ConfigSpec, ConfigureContext}; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::util::serde::HandlerExtSerde; +use crate::util::PathOrUrl; use crate::Error; pub fn dependency() -> ParentHandler { @@ -42,6 +45,16 @@ impl Map for Dependencies { pub struct DepInfo { pub description: Option, pub optional: bool, + pub s9pk: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string")] + pub title: InternedString, } #[derive(Deserialize, Serialize, Parser, TS)] @@ -52,11 +65,20 @@ pub struct ConfigureParams { dependency_id: PackageId, } pub fn configure() -> ParentHandler { - ParentHandler::new().root_handler( - from_fn_async(configure_impl) - .with_inherited(|params, _| params) - .no_cli(), - ) + ParentHandler::new() + .root_handler( + from_fn_async(configure_impl) + .with_inherited(|params, _| params) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "dry", + from_fn_async(configure_dry) + .with_inherited(|params, _| params) + .with_display_serializable() + .with_call_remote::(), + ) } pub async fn configure_impl( @@ -92,6 +114,17 @@ pub async fn configure_impl( Ok(()) } +pub async fn configure_dry( + ctx: RpcContext, + _: Empty, + ConfigureParams { + dependent_id, + dependency_id, + }: ConfigureParams, +) -> Result { + configure_logic(ctx.clone(), (dependent_id, dependency_id.clone())).await +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConfigDryRes { diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index 3a13c5dca..73aca4010 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -168,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") diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index 142301a74..8f45b0d4f 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -106,8 +106,11 @@ impl BackupMountGuard { ) })?; } - let encrypted_guard = - TmpMountGuard::mount(&BackupFS::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.path().join("metadata.json"); let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() { @@ -148,8 +151,23 @@ impl BackupMountGuard { } #[instrument(skip_all)] - pub fn package_backup(self: &Arc, id: &PackageId) -> SubPath> { - SubPath::new(self.clone(), id) + pub async fn package_backup( + self: &Arc, + id: &PackageId, + ) -> 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)] diff --git a/core/startos/src/disk/mount/filesystem/backupfs.rs b/core/startos/src/disk/mount/filesystem/backupfs.rs index 9ef258f34..254abde20 100644 --- a/core/startos/src/disk/mount/filesystem/backupfs.rs +++ b/core/startos/src/disk/mount/filesystem/backupfs.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display}; use std::os::unix::ffi::OsStrExt; use std::path::Path; @@ -12,10 +13,15 @@ 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) -> Self { - BackupFS { data_dir, password } + 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 @@ -26,9 +32,16 @@ impl + Send + Sync, Password: fmt::Display + Send + Sync> F } fn mount_options(&self) -> impl IntoIterator { [ - format!("password={}", self.password), - format!("file-size-padding=0.05"), + 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)) diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index d6e7e3da1..a2d577226 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -74,7 +74,7 @@ impl MountGuard { } 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 @@ -96,7 +96,7 @@ 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.unwrap() }); } } } diff --git a/core/startos/src/disk/mount/util.rs b/core/startos/src/disk/mount/util.rs index e93ceb7dd..674f33304 100644 --- a/core/startos/src/disk/mount/util.rs +++ b/core/startos/src/disk/mount/util.rs @@ -23,7 +23,7 @@ pub async fn bind, P1: AsRef>( .status() .await?; if is_mountpoint.success() { - unmount(dst.as_ref()).await?; + unmount(dst.as_ref(), true).await?; } tokio::fs::create_dir_all(&src).await?; tokio::fs::create_dir_all(&dst).await?; @@ -41,11 +41,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("-Rl") - .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/firmware.rs b/core/startos/src/firmware.rs index 7e7b9d70f..a70cf9e47 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -13,8 +13,8 @@ use crate::util::Invoke; use crate::PLATFORM; /// Part of the Firmware, look there for more about -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct VersionMatcher { /// Strip this prefix on the version matcher semver_prefix: Option, @@ -27,8 +27,8 @@ 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)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct Firmware { id: String, /// This is the platform(s) the firmware was built for @@ -49,6 +49,7 @@ pub fn display_firmware_update_result(result: RequiresReboot) { } } +#[instrument] pub async fn check_for_firmware_update() -> Result, Error> { let system_product_name = String::from_utf8( Command::new("dmidecode") @@ -118,6 +119,7 @@ pub async fn check_for_firmware_update() -> Result, Error> { /// 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"); diff --git a/core/startos/src/hostname.rs b/core/startos/src/hostname.rs index c4332354c..36bb5d8a4 100644 --- a/core/startos/src/hostname.rs +++ b/core/startos/src/hostname.rs @@ -1,3 +1,5 @@ +use imbl_value::InternedString; +use lazy_format::lazy_format; use rand::{thread_rng, Rng}; use tokio::process::Command; use tracing::instrument; @@ -5,7 +7,7 @@ use tracing::instrument; use crate::util::Invoke; use crate::{Error, ErrorKind}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] -pub struct Hostname(pub String); +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 48cb24c9a..e6b7be598 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -65,7 +65,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 { @@ -235,7 +235,7 @@ impl InitPhases { 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(10)), + launch_service_network: handle.add_phase("Launching service intranet".into(), Some(1)), run_migrations: handle.add_phase("Running migrations".into(), Some(10)), validate_db: handle.add_phase("Validating database".into(), Some(1)), postinit: if Path::new("/media/startos/config/postinit.sh").exists() { @@ -398,6 +398,20 @@ pub async fn init( Command::new("update-ca-certificates") .invoke(crate::ErrorKind::OpenSsl) .await?; + if tokio::fs::metadata("/home/kiosk/profile").await.is_ok() { + Command::new("certutil") + .arg("-A") + .arg("-n") + .arg("StartOS Local Root CA") + .arg("-t") + .arg("TCu,Cuw,Tuw") + .arg("-i") + .arg("/usr/local/share/ca-certificates/startos-root-ca.crt") + .arg("-d") + .arg("/home/kiosk/fx-profile") + .invoke(ErrorKind::OpenSsl) + .await?; + } load_ca_cert.complete(); load_wifi.start(); @@ -422,6 +436,12 @@ pub async fn init( tokio::fs::remove_dir_all(&tmp_var).await?; } crate::disk::mount::util::bind(&tmp_var, "/var/tmp", false).await?; + let downloading = cfg + .datadir() + .join(format!("package-data/archive/downloading")); + if tokio::fs::metadata(&downloading).await.is_ok() { + tokio::fs::remove_dir_all(&downloading).await?; + } let tmp_docker = cfg .datadir() .join(format!("package-data/tmp/{CONTAINER_TOOL}")); @@ -554,33 +574,54 @@ pub struct InitProgressRes { 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 { - if let Err(e) = 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; + 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; + } } - } - ws.normal_close("complete").await?; - - Ok::<_, Error>(()) - } - .await + 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 in init progress websocket: {e}"); + tracing::error!("error closing init progress websocket: {e}"); tracing::debug!("{e:?}"); } }, diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 18591dc8d..eefd6eb66 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -12,11 +12,12 @@ use itertools::Itertools; use models::VersionString; use reqwest::header::{HeaderMap, CONTENT_LENGTH}; use reqwest::Url; -use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::yajrc::{GenericRpcMethod, RpcError}; use rpc_toolkit::HandlerArgs; use rustyline_async::ReadlineEvent; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use tracing::instrument; use ts_rs::TS; @@ -111,6 +112,7 @@ impl std::fmt::Display for MinMax { #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct InstallParams { #[ts(type = "string")] registry: Url, @@ -156,7 +158,7 @@ pub async fn install( .services .install( ctx.clone(), - || asset.deserialize_s9pk(ctx.client.clone()), + || asset.deserialize_s9pk_buffered(ctx.client.clone()), None::, None, ) @@ -187,7 +189,7 @@ pub async fn sideload( SideloadParams { session }: SideloadParams, ) -> Result { let (upload, file) = upload(&ctx, session.clone()).await?; - let (err_send, err_recv) = oneshot::channel(); + let (err_send, 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))); @@ -201,11 +203,14 @@ pub async fn sideload( use axum::extract::ws::Message; async move { if let Err(e) = async { + type RpcResponse = rpc_toolkit::yajrc::RpcResponse< + GenericRpcMethod<&'static str, (), FullProgress>, + >; tokio::select! { res = async { while let Some(progress) = progress_listener.next().await { ws.send(Message::Text( - serde_json::to_string(&Ok::<_, ()>(progress)) + serde_json::to_string(&progress) .with_kind(ErrorKind::Serialization)?, )) .await @@ -215,12 +220,8 @@ pub async fn sideload( } => res?, err = err_recv => { if let Ok(e) = err { - ws.send(Message::Text( - serde_json::to_string(&Err::<(), _>(e)) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; + ws.close_result(Err::<&str, _>(e.clone_output())).await?; + return Err(e) } } } @@ -258,7 +259,7 @@ pub async fn sideload( } .await { - let _ = err_send.send(RpcError::from(e.clone_output())); + let _ = err_send.send(e.clone_output()); tracing::error!("Error sideloading package: {e}"); tracing::debug!("{e:?}"); } @@ -409,11 +410,17 @@ pub async fn cli_install( tokio::select! { msg = ws.next() => { if let Some(msg) = msg { - if let Message::Text(t) = msg.with_kind(ErrorKind::Network)? { - progress = - serde_json::from_str::>(&t) - .with_kind(ErrorKind::Deserialization)??; - bar.update(&progress); + match msg.with_kind(ErrorKind::Network)? { + Message::Text(t) => { + progress = + serde_json::from_str::(&t) + .with_kind(ErrorKind::Deserialization)?; + bar.update(&progress); + } + Message::Close(Some(c)) if c.code != CloseCode::Normal => { + return Err(Error::new(eyre!("{}", c.reason), ErrorKind::Network)) + } + _ => (), } } else { break; diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 4882d998e..feeb5a647 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -224,6 +224,18 @@ pub fn server() -> ParentHandler { }) .with_call_remote::(), ) + .subcommand( + "set-smtp", + from_fn_async(system::set_system_smtp) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "clear-smtp", + from_fn_async(system::clear_system_smtp) + .no_display() + .with_call_remote::(), + ) } pub fn package() -> ParentHandler { diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 751dd243a..9cf234f5f 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -184,7 +184,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 @@ -202,7 +208,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) } } diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 99f019d5a..480f7a24c 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -123,7 +123,11 @@ impl LxcManager { 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")).await?; + unmount( + Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"), + true, + ) + .await?; if tokio_stream::wrappers::ReadDirStream::new( tokio::fs::read_dir(&rootfs_path).await?, ) @@ -264,9 +268,10 @@ impl LxcContainer { .invoke(ErrorKind::Docker) .await?, )?; - let out_str = output.trim(); - if !out_str.is_empty() { - return Ok(out_str.parse()?); + 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( @@ -284,6 +289,11 @@ impl LxcContainer { #[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?; diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index fd60894db..9b04afb38 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -51,6 +51,11 @@ impl HasLoggedOutSessions { for sid in &to_log_out { ctx.open_authed_continuations.kill(sid) } + 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(); @@ -110,20 +115,29 @@ impl HasValidSession { ctx: &RpcContext, ) -> Result { let session_hash = session_token.hashed(); - 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?; + 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(SessionType::Session(session_token))) } diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index dca0418e5..4e5f0e037 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -7,7 +7,6 @@ use serde::Deserialize; use crate::context::RpcContext; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] sync_db: bool, @@ -37,10 +36,11 @@ impl Middleware for SyncDb { async fn process_http_response(&mut self, context: &RpcContext, response: &mut Response) { if let Err(e) = async { if self.sync_db { - response.headers_mut().append( - "X-Patch-Sequence", - HeaderValue::from_str(&context.db.sequence().await.to_string())?, - ); + 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); } Ok::<_, InvalidHeaderValue>(()) } diff --git a/core/startos/src/net/dns.rs b/core/startos/src/net/dns.rs index ba69b6c16..016e5de9f 100644 --- a/core/startos/src/net/dns.rs +++ b/core/startos/src/net/dns.rs @@ -34,7 +34,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) @@ -98,16 +98,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( diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index d9e2f4206..9b16441ce 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -1,7 +1,13 @@ +use std::fmt; +use std::str::FromStr; + +use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use torut::onion::OnionAddressV3; use ts_rs::TS; +use crate::prelude::*; + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] #[serde(rename_all = "camelCase")] #[serde(tag = "kind")] @@ -11,4 +17,32 @@ pub enum HostAddress { #[ts(type = "string")] address: OnionAddressV3, }, + Domain { + #[ts(type = "string")] + address: InternedString, + }, +} + +impl FromStr for HostAddress { + type Err = Error; + fn from_str(s: &str) -> Result { + if let Some(addr) = s.strip_suffix(".onion") { + Ok(HostAddress::Onion { + address: addr + .parse::() + .with_kind(ErrorKind::ParseUrl)?, + }) + } else { + Ok(HostAddress::Domain { address: s.into() }) + } + } +} + +impl fmt::Display for HostAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Onion { address } => write!(f, "{address}"), + Self::Domain { address } => write!(f, "{address}"), + } + } } diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index 6cbb2dfd5..175fe3e83 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -40,6 +40,10 @@ impl Host { hostname_info: BTreeMap::new(), } } + pub fn addresses(&self) -> impl Iterator { + // TODO: handle primary + self.addresses.iter() + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 270c7ca09..b7a8022b4 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use imbl::OrdMap; +use imbl_value::InternedString; use models::{HostId, OptionExt, PackageId}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; @@ -28,6 +29,7 @@ pub struct PreInitNetController { tor: TorController, vhost: VHostController, os_bindings: Vec>, + server_hostnames: Vec>, } impl PreInitNetController { #[instrument(skip_all)] @@ -43,6 +45,7 @@ impl PreInitNetController { tor: TorController::new(tor_control, tor_socks), vhost: VHostController::new(db), os_bindings: Vec::new(), + server_hostnames: Vec::new(), }; res.add_os_bindings(hostname, os_tor_key).await?; Ok(res) @@ -58,62 +61,34 @@ impl PreInitNetController { MaybeUtf8String("h2".into()), ])); - // Internal DNS - self.vhost - .add( - Some("embassy".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?; - - // LAN IP - self.os_bindings.push( - self.vhost - .add(None, 443, ([127, 0, 0, 1], 80).into(), alpn.clone()) - .await?, - ); - - // localhost - self.os_bindings.push( - self.vhost - .add( - Some("localhost".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - self.os_bindings.push( - self.vhost - .add( - Some(hostname.no_dot_host_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); + self.server_hostnames = vec![ + // LAN IP + None, + // Internal DNS + Some("embassy".into()), + Some("startos".into()), + // localhost + Some("localhost".into()), + Some(hostname.no_dot_host_name()), + // LAN mDNS + Some(hostname.local_domain_name()), + ]; - // LAN mDNS - self.os_bindings.push( - self.vhost - .add( - Some(hostname.local_domain_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); + for hostname in self.server_hostnames.iter().cloned() { + self.os_bindings.push( + self.vhost + .add(hostname, 443, ([127, 0, 0, 1], 80).into(), alpn.clone()) + .await?, + ); + } // Tor self.os_bindings.push( self.vhost .add( - Some(tor_key.public().get_onion_address().to_string()), + Some(InternedString::from_display( + &tor_key.public().get_onion_address(), + )), 443, ([127, 0, 0, 1], 80).into(), alpn.clone(), @@ -143,6 +118,7 @@ pub struct NetController { pub(super) dns: DnsController, pub(super) forward: LanPortForwardController, pub(super) os_bindings: Vec>, + pub(super) server_hostnames: Vec>, } impl NetController { @@ -152,6 +128,7 @@ impl NetController { tor, vhost, os_bindings, + server_hostnames, }: PreInitNetController, dns_bind: &[SocketAddr], ) -> Result { @@ -162,6 +139,7 @@ impl NetController { dns: DnsController::init(dns_bind).await?, forward: LanPortForwardController::new(), os_bindings, + server_hostnames, }; res.os_bindings .push(res.dns.add(None, HOST_IP.into()).await?); @@ -189,7 +167,15 @@ impl NetController { #[derive(Default, Debug)] struct HostBinds { - lan: BTreeMap, Vec>)>, + lan: BTreeMap< + u16, + ( + LanInfo, + Option, + BTreeSet, + Vec>, + ), + >, tor: BTreeMap, Vec>)>, } @@ -234,20 +220,40 @@ impl NetService { .await?; self.update(id, host).await } + pub async fn clear_bindings(&mut self) -> Result<(), Error> { - // TODO BLUJ - Ok(()) + let ctrl = self.net_controller()?; + let mut errors = ErrorCollection::new(); + for (_, binds) in std::mem::take(&mut self.binds) { + for (_, (lan, _, hostnames, rc)) in binds.lan { + drop(rc); + if let Some(external) = lan.assigned_ssl_port { + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } + } + if let Some(external) = lan.assigned_port { + ctrl.forward.gc(external).await?; + } + } + for (addr, (_, rcs)) in binds.tor { + drop(rcs); + errors.handle(ctrl.tor.gc(Some(addr), None).await); + } + } + std::mem::take(&mut self.dns); + errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); + errors.into_result() } async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> { let ctrl = self.net_controller()?; let mut hostname_info = BTreeMap::new(); - let binds = { - if !self.binds.contains_key(&id) { - self.binds.insert(id.clone(), Default::default()); - } - self.binds.get_mut(&id).unwrap() - }; + let binds = self.binds.entry(id.clone()).or_default(); + let peek = ctrl.db.peek().await; // LAN @@ -256,37 +262,73 @@ impl NetService { let hostname = server_info.as_hostname().de()?; for (port, bind) in &host.bindings { let old_lan_bind = binds.lan.remove(port); - let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external); let lan_bind = old_lan_bind - .filter(|(external, ssl, _)| ssl == &bind.options.add_ssl && bind.lan == *external); // only keep existing binding if relevant details match + .as_ref() + .filter(|(external, ssl, _, _)| { + ssl == &bind.options.add_ssl && bind.lan == *external + }) + .cloned(); // only keep existing binding if relevant details match if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() { let new_lan_bind = if let Some(b) = lan_bind { b } else { - let mut rcs = Vec::with_capacity(2); + let mut rcs = Vec::with_capacity(2 + host.addresses.len()); + let mut hostnames = BTreeSet::new(); if let Some(ssl) = &bind.options.add_ssl { let external = bind .lan .assigned_ssl_port .or_not_found("assigned ssl port")?; - rcs.push( - ctrl.vhost - .add( - None, - external, - (self.ip, *port).into(), - 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) - } - }, - ) - .await?, - ); + let target = (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() { + rcs.push( + ctrl.vhost + .add(hostname, external, target, connect_ssl.clone()) + .await?, + ); + } + for address in host.addresses() { + match address { + HostAddress::Onion { address } => { + let hostname = InternedString::from_display(address); + if hostnames.insert(hostname.clone()) { + rcs.push( + ctrl.vhost + .add( + Some(hostname), + external, + target, + connect_ssl.clone(), + ) + .await?, + ); + } + } + HostAddress::Domain { address } => { + if hostnames.insert(address.clone()) { + rcs.push( + ctrl.vhost + .add( + Some(address.clone()), + external, + target, + connect_ssl.clone(), + ) + .await?, + ); + } + } + } + } } if let Some(security) = bind.options.secure { if bind.options.add_ssl.is_some() && security.ssl { @@ -297,7 +339,7 @@ impl NetService { rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?); } } - (bind.lan, bind.options.add_ssl.clone(), rcs) + (bind.lan, bind.options.add_ssl.clone(), hostnames, rcs) }; let mut bind_hostname_info: Vec = hostname_info.remove(port).unwrap_or_default(); @@ -337,9 +379,14 @@ impl NetService { hostname_info.insert(*port, bind_hostname_info); binds.lan.insert(*port, new_lan_bind); } - if let Some(lan) = old_lan_port { + if let Some((lan, _, hostnames, _)) = old_lan_bind { if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } } if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; @@ -347,18 +394,23 @@ impl NetService { } } let mut removed = BTreeSet::new(); - binds.lan.retain(|internal, (external, _, _)| { + binds.lan.retain(|internal, (external, _, hostnames, _)| { if host.bindings.contains_key(internal) { true } else { - removed.insert(*external); + removed.insert((*external, std::mem::take(hostnames))); false } }); - for lan in removed { + for (lan, hostnames) in removed { if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } } if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; @@ -401,48 +453,44 @@ impl NetService { ); } } + let mut keep_tor_addrs = BTreeSet::new(); - for addr in match host.kind { - HostKind::Multi => { - // itertools::Either::Left( - host.addresses.iter() - // ) - } // HostKind::Single | HostKind::Static => itertools::Either::Right(&host.primary), - } { - match addr { - HostAddress::Onion { address } => { - keep_tor_addrs.insert(address); - let old_tor_bind = binds.tor.remove(address); - let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds); - let new_tor_bind = if let Some(tor_bind) = tor_bind { - tor_bind - } else { - let key = peek - .as_private() - .as_key_store() - .as_onion() - .get_key(address)?; - let rcs = ctrl - .tor - .add(key, tor_binds.clone().into_iter().collect()) - .await?; - (tor_binds.clone(), rcs) - }; - 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: address.to_string(), - port: ports.non_ssl, - ssl_port: ports.ssl, - }, - }); - hostname_info.insert(*internal, bind_hostname_info); - } - binds.tor.insert(address.clone(), new_tor_bind); - } + for tor_addr in host.addresses().filter_map(|a| { + if let HostAddress::Onion { address } = a { + Some(address) + } else { + None } + }) { + keep_tor_addrs.insert(tor_addr); + let old_tor_bind = binds.tor.remove(tor_addr); + let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds); + let new_tor_bind = if let Some(tor_bind) = tor_bind { + tor_bind + } else { + let key = peek + .as_private() + .as_key_store() + .as_onion() + .get_key(tor_addr)?; + let rcs = ctrl + .tor + .add(key, tor_binds.clone().into_iter().collect()) + .await?; + (tor_binds.clone(), rcs) + }; + 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); + } + binds.tor.insert(tor_addr.clone(), new_tor_bind); } for addr in binds.tor.keys() { if !keep_tor_addrs.contains(addr) { @@ -462,26 +510,10 @@ impl NetService { 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 (_, binds) in std::mem::take(&mut self.binds) { - for (_, (lan, _, rc)) in binds.lan { - drop(rc); - if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; - } - if let Some(external) = lan.assigned_port { - ctrl.forward.gc(external).await?; - } - } - for (addr, (_, rcs)) in binds.tor { - drop(rcs); - errors.handle(ctrl.tor.gc(Some(addr), None).await); - } - } - std::mem::take(&mut self.dns); - errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); - errors.into_result() + self.clear_bindings().await?; + drop(ctrl); + Ok(()) } else { tracing::warn!("NetService dropped after NetController is shutdown"); Err(Error::new( @@ -495,11 +527,11 @@ impl NetService { self.ip } - pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { + pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result { let host_id_binds = self.binds.get_key_value(&host_id); match host_id_binds { Some((_, binds)) => { - if let Some((lan, _, _)) = binds.lan.get(&internal_port) { + if let Some((lan, _, _, _)) = binds.lan.get(&internal_port) { Ok(*lan) } else { Err(Error::new( diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index dbe228ef2..b1824140b 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -67,7 +67,6 @@ pub struct ServiceInterface { pub name: String, pub description: String, pub has_primary: bool, - pub disabled: bool, pub masked: bool, pub address_info: AddressInfo, #[serde(rename = "type")] diff --git a/core/startos/src/net/ssl.rs b/core/startos/src/net/ssl.rs index 382006072..29bcd9652 100644 --- a/core/startos/src/net/ssl.rs +++ b/core/startos/src/net/ssl.rs @@ -1,13 +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; @@ -17,6 +17,7 @@ use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; use openssl::*; use patch_db::HasModel; use serde::{Deserialize, Serialize}; +use tokio::time::Instant; use tracing::instrument; use crate::account::AccountInfo; @@ -126,12 +127,18 @@ impl Model { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[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, @@ -144,6 +151,16 @@ impl FullchainCertData { 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. @@ -155,6 +172,26 @@ fn unix_time(time: SystemTime) -> time_t { .unwrap_or_default() } +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")] @@ -162,6 +199,12 @@ pub struct PKeyPair { #[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 { @@ -170,6 +213,14 @@ pub struct CertPair { #[serde(with = "crate::util::serde::pem")] pub nistp256: X509, } +impl CertPair { + 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())?, + )) + } +} pub async fn root_ca_start_time() -> Result { Ok(if check_time_is_synchronized().await? { diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 3e5f0997d..f1da91851 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -1,5 +1,8 @@ +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; @@ -8,36 +11,51 @@ use axum::extract::{self as x, Request}; use axum::response::Response; use axum::routing::{any, get, post}; use axum::Router; +use base64::display::Base64Display; use digest::Digest; use futures::future::ready; -use http::header::ACCEPT_ENCODING; +use http::header::{ + ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, + CONTENT_RANGE, CONTENT_TYPE, ETAG, RANGE, +}; use http::request::Parts as RequestParts; -use http::{Method, StatusCode}; +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::{Context, HttpServer, Server}; -use tokio::io::BufReader; +use sqlx::query; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader}; use tokio_util::io::ReaderStream; +use url::Url; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; +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::{ - diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, -}; +use crate::util::net::SyncBody; +use crate::util::serde::BASE64; +use crate::{diagnostic_api, init_api, install_api, main_api, setup_api}; 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"]; + #[cfg(all(feature = "daemon", not(feature = "test")))] const EMBEDDED_UIS: Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); @@ -97,7 +115,7 @@ pub fn rpc_router>( 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 @@ -111,7 +129,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result { .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) + FileData::from_embedded(&request_parts, file)?.into_response(&request_parts) } else { Ok(not_found()) } @@ -161,14 +179,35 @@ pub fn init_ui_router(ctx: InitContext) -> Router { } pub fn main_ui_router(ctx: RpcContext) -> Router { - rpc_router( - ctx.clone(), + 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()), + .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 + .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) + } + }), ) - // TODO: cert .fallback(any(|request: Request| async move { serve_ui(request, UiMode::Main).unwrap_or_else(server_error) })) @@ -179,29 +218,137 @@ pub fn refresher() -> Router { 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, } .into_response(&request.into_parts().0) .unwrap_or_else(server_error) })) } +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, + &ctx.datadir + .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( + ctx.datadir + .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() -> Fut, - Fut: Future> + Send + Sync, + F: FnOnce(Request) -> Fut, + Fut: Future> + Send, >( ctx: &RpcContext, - parts: &RequestParts, + request: Request, f: F, ) -> Result { - if let Err(e) = HasValidSession::from_header(parts.headers.get(http::header::COOKIE), ctx).await + if let Err(e) = + HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await { - Ok(unauthorized(e, parts.uri.path())) + Ok(unauthorized(e, request.uri().path())) } else { - f().await + f(request).await } } @@ -268,44 +415,117 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result { .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: 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(), + content_range, + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, e_tag: file.metadata().map(|metadata| { e_tag( path, @@ -323,11 +543,28 @@ impl FileData { mime: MimeGuess::from_path(path) .first() .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() @@ -338,12 +575,23 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = open_file(path).await?; + 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 content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, metadata.len())) + .transpose()?; + let e_tag = Some(e_tag( path, format!( @@ -357,51 +605,123 @@ impl FileData { .as_bytes(), )); - let (len, data) = if encoding == Some("gzip") { - ( - None, - Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))), - ) + 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::from_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().into()), - }) + digest: None, + })) + } + + 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(http::header::ETAG, &**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::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 self.e_tag.is_some() @@ -411,14 +731,13 @@ impl FileData { .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) diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index e6a9d5b21..9fc7c8384 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -13,6 +13,7 @@ use http::Uri; use imbl_value::InternedString; use models::ResultExt; use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{Mutex, RwLock}; use tokio_rustls::rustls::pki_types::{ @@ -27,7 +28,7 @@ use ts_rs::TS; use crate::db::model::Database; use crate::net::static_server::server_error; use crate::prelude::*; -use crate::util::io::BackTrackingReader; +use crate::util::io::BackTrackingIO; use crate::util::serde::MaybeUtf8String; // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 @@ -46,7 +47,7 @@ impl VHostController { #[instrument(skip_all)] pub async fn add( &self, - hostname: Option, + hostname: Option, external: u16, target: SocketAddr, connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn @@ -70,7 +71,7 @@ impl VHostController { Ok(rc?) } #[instrument(skip_all)] - pub async fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { + 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?; @@ -102,7 +103,7 @@ impl Default for AlpnInfo { } struct VHostServer { - mapping: Weak, BTreeMap>>>>, + mapping: Weak, BTreeMap>>>>, _thread: NonDetachingJoinHandle<()>, } impl VHostServer { @@ -129,8 +130,7 @@ impl VHostServer { tracing::debug!("{e:?}"); } - let mut stream = BackTrackingReader::new(stream); - stream.start_buffering(); + let mut stream = BackTrackingIO::new(stream); let mapping = mapping.clone(); let db = db.clone(); tokio::spawn(async move { @@ -156,6 +156,7 @@ impl VHostServer { .and_then(|host| host.to_str().ok()); let uri = Uri::from_parts({ let mut parts = req.uri().to_owned().into_parts(); + parts.scheme = Some("https".parse()?); parts.authority = host.map(FromStr::from_str).transpose()?; parts })?; @@ -179,7 +180,7 @@ impl VHostServer { } }; let target_name = - mid.client_hello().server_name().map(|s| s.to_owned()); + mid.client_hello().server_name().map(|s| s.into()); let target = { let mapping = mapping.read().await; mapping @@ -208,9 +209,7 @@ impl VHostServer { let mut tcp_stream = TcpStream::connect(target.addr).await?; let hostnames = target_name - .as_ref() .into_iter() - .map(InternedString::intern) .chain( db.peek() .await @@ -315,8 +314,12 @@ impl VHostServer { ) .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 mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -324,7 +327,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut target_stream, @@ -337,8 +339,12 @@ impl VHostServer { { 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 mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -346,7 +352,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut tcp_stream, @@ -355,8 +360,12 @@ impl VHostServer { } 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 mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -364,7 +373,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut tcp_stream, @@ -405,7 +413,11 @@ impl VHostServer { .into(), }) } - async fn add(&self, hostname: Option, target: TargetInfo) -> Result, Error> { + async fn add( + &self, + hostname: Option, + target: TargetInfo, + ) -> Result, Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { let mut writable = mapping.write().await; let mut targets = writable.remove(&hostname).unwrap_or_default(); @@ -424,7 +436,7 @@ impl VHostServer { )) } } - async fn gc(&self, hostname: Option) -> Result<(), Error> { + async fn gc(&self, hostname: Option) -> Result<(), Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { let mut writable = mapping.write().await; let mut targets = writable.remove(&hostname).unwrap_or_default(); diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 28fd2a3be..3d80f6cbd 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -147,6 +147,23 @@ pub async fn execute( overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none()); + if !overwrite + && (disk + .guid + .as_ref() + .map_or(false, |g| g.starts_with("EMBASSY_")) + || disk + .partitions + .iter() + .flat_map(|p| p.guid.as_ref()) + .any(|g| g.starts_with("EMBASSY_"))) + { + return Err(Error::new( + eyre!("installing over versions before 0.3.6 is unsupported"), + ErrorKind::InvalidRequest, + )); + } + let part_info = partition(&mut disk, overwrite).await?; if let Some(efi) = &part_info.efi { diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index 5a2e5ef27..cc3257132 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -18,7 +18,9 @@ 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} [{bytes}/{total_bytes}] [{binary_bytes_per_sec} {eta}]").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(); } @@ -38,7 +40,7 @@ impl Progress { pub fn new() -> Self { Progress::NotStarted(()) } - pub fn update_bar(self, bar: &ProgressBar) { + pub fn update_bar(self, bar: &ProgressBar, bytes: bool) { match self { Self::NotStarted(()) => { bar.set_style(SPINNER.clone()); @@ -51,7 +53,11 @@ impl Progress { bar.finish(); } Self::Progress { done, total: None } => { - bar.set_style(BYTES.clone()); + if bytes { + bar.set_style(BYTES.clone()); + } else { + bar.set_style(STEPS.clone()); + } bar.set_position(done); bar.tick(); } @@ -59,7 +65,11 @@ impl Progress { done, total: Some(total), } => { - bar.set_style(PERCENTAGE.clone()); + if bytes { + bar.set_style(PERCENTAGE_BYTES.clone()); + } else { + bar.set_style(PERCENTAGE.clone()); + } bar.set_position(done); bar.set_length(total); bar.tick(); @@ -490,7 +500,7 @@ impl PhasedProgressBar { ); } } - progress.overall.update_bar(&self.overall); + 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 { @@ -499,7 +509,7 @@ impl PhasedProgressBar { None } }) { - progress.update_bar(bar); + progress.update_bar(bar, true); } } } diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index cd795e5cd..8125580a4 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -46,7 +46,7 @@ fn signers_api() -> ParentHandler { .with_metadata("admin", Value::Bool(true)) .no_cli(), ) - .subcommand("add", from_fn_async(cli_add_signer).no_display()) + .subcommand("add", from_fn_async(cli_add_signer)) } impl Model> { @@ -71,7 +71,7 @@ impl Model> { .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<(), Error> { + pub fn add_signer(&mut self, signer: &SignerInfo) -> Result { if let Some((guid, s)) = self .as_entries()? .into_iter() @@ -89,7 +89,9 @@ impl Model> { ErrorKind::InvalidRequest, )); } - self.insert(&Guid::new(), signer) + let id = Guid::new(); + self.insert(&id, signer)?; + Ok(id) } } @@ -122,7 +124,7 @@ pub fn display_signers(params: WithIoFormat, signers: BTreeMap Result<(), Error> { +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 @@ -155,7 +157,7 @@ pub async fn cli_add_signer( }, .. }: HandlerArgs, -) -> Result<(), Error> { +) -> Result { let signer = SignerInfo { name, contact, @@ -165,15 +167,16 @@ pub async fn cli_add_signer( TypedPatchDb::::load(PatchDb::open(database).await?) .await? .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) - .await?; + .await } else { - ctx.call_remote::( - &parent_method.into_iter().chain(method).join("."), - to_value(&signer)?, + from_value( + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&signer)?, + ) + .await?, ) - .await?; } - Ok(()) } #[derive(Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index ab251d2aa..fb6dd59fc 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -1,6 +1,8 @@ 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; @@ -13,13 +15,16 @@ 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::Section; +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, @@ -67,4 +72,42 @@ impl RegistryAsset { ) .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 index 27655c4a8..4707bf809 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/registry/auth.rs @@ -27,7 +27,6 @@ use crate::util::serde::Base64; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] admin: bool, @@ -75,9 +74,7 @@ pub struct RegistryAdminLogRecord { pub key: AnyVerifyingKey, } -#[derive(Serialize, Deserialize)] pub struct SignatureHeader { - #[serde(flatten)] pub commitment: RequestCommitment, pub signer: AnyVerifyingKey, pub signature: AnySignature, @@ -93,14 +90,9 @@ impl SignatureHeader { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::Utf8)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { - commitment: RequestCommitment::from_query(&url)?, + commitment: RequestCommitment::from_query(&header)?, signer: query.get("signer").or_not_found("signer")?.parse()?, signature: query.get("signature").or_not_found("signature")?.parse()?, }) diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 64a157073..d3eaf3691 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -200,6 +200,19 @@ impl CallRemote for CliContext { .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) @@ -210,7 +223,7 @@ impl CallRemote for CliContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } @@ -247,6 +260,19 @@ impl CallRemote for RpcContext { .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) @@ -257,7 +283,7 @@ impl CallRemote for RpcContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index 9a357358a..172348a10 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -54,12 +54,7 @@ impl DeviceInfo { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header_value(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::ParseUrl)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { os: OsInfo { version: query @@ -151,7 +146,6 @@ impl From<&RpcContext> for HardwareInfo { } #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] get_device_info: bool, diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 1039264df..d34ebb841 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use axum::Router; use futures::future::ready; +use imbl_value::InternedString; use models::DataUrl; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ 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::package::index::{Category, PackageIndex}; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; @@ -45,6 +46,7 @@ impl RegistryDatabase {} #[model = "Model"] #[ts(export)] pub struct FullIndex { + pub name: Option, pub icon: Option>, pub package: PackageIndex, pub os: OsIndex, @@ -55,6 +57,25 @@ pub async fn get_full_index(ctx: RegistryContext) -> Result { ctx.db.peek().await.into_index().de() } +#[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()?, + }) +} + pub fn registry_api() -> ParentHandler { ParentHandler::new() .subcommand( @@ -63,6 +84,12 @@ pub fn registry_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) + .subcommand( + "info", + from_fn_async(get_info) + .with_display_serializable() + .with_call_remote::(), + ) .subcommand("os", os::os_api::()) .subcommand("package", package::package_api::()) .subcommand("admin", admin::admin_api::()) diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 6108dd5bc..d609063ea 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -2,7 +2,9 @@ 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}; @@ -12,7 +14,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker}; +use crate::progress::FullProgressTracker; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -26,26 +28,25 @@ 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; -use crate::util::VersionString; pub fn add_api() -> ParentHandler { ParentHandler::new() .subcommand( "iso", from_fn_async(add_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(add_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(add_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } @@ -54,7 +55,8 @@ pub fn add_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddAssetParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, #[ts(type = "string")] pub platform: InternedString, #[ts(type = "string")] @@ -107,6 +109,7 @@ async fn add_asset( ) .upsert(&platform, || { Ok(RegistryAsset { + published_at: Utc::now(), url, commitment: commitment.clone(), signatures: HashMap::new(), @@ -152,7 +155,7 @@ pub struct CliAddAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: VersionString, + pub version: Version, pub file: PathBuf, pub url: Url, } @@ -207,11 +210,18 @@ pub async fn cli_add_asset( hash: Base64(*blake3.as_bytes()), size, }; - let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + 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)), diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index b185cf6a4..ad0010dca 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -3,6 +3,7 @@ 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; @@ -21,7 +22,6 @@ 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; -use crate::util::VersionString; pub fn get_api() -> ParentHandler { ParentHandler::new() @@ -37,7 +37,8 @@ pub fn get_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct GetOsAssetParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, #[ts(type = "string")] pub platform: InternedString, } @@ -91,7 +92,7 @@ pub async fn get_squashfs( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] pub struct CliGetOsAssetParams { - pub version: VersionString, + pub version: Version, pub platform: InternedString, #[arg(long = "download", short = 'd')] pub download: Option, diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 8bf1cfeb5..18b603daf 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -3,6 +3,7 @@ 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}; @@ -23,26 +24,25 @@ 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; -use crate::util::VersionString; pub fn sign_api() -> ParentHandler { ParentHandler::new() .subcommand( "iso", from_fn_async(sign_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(sign_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(sign_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } @@ -51,7 +51,8 @@ pub fn sign_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct SignAssetParams { - version: VersionString, + #[ts(type = "string")] + version: Version, #[ts(type = "string")] platform: InternedString, #[ts(skip)] @@ -137,7 +138,7 @@ pub struct CliSignAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: VersionString, + pub version: Version, pub file: PathBuf, } @@ -189,7 +190,11 @@ pub async fn cli_sign_asset( hash: Base64(*blake3.as_bytes()), size, }; - let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + let signature = AnySignature::Ed25519(Ed25519.sign_commitment( + ctx.developer_key()?, + &commitment, + SIG_CONTEXT, + )?); sign_phase.complete(); index_phase.start(); diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs index 0b1ca5b89..b61cb8f96 100644 --- a/core/startos/src/registry/os/index.rs +++ b/core/startos/src/registry/os/index.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use exver::VersionRange; +use exver::{Version, VersionRange}; use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -10,14 +10,28 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::signer::commitment::blake3::Blake3Commitment; use crate::rpc_continuations::Guid; -use crate::util::VersionString; #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct OsIndex { - pub versions: BTreeMap, + 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)] diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 9ebe8a696..4c0568a80 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use chrono::Utc; use clap::Parser; -use exver::VersionRange; +use exver::{Version, VersionRange}; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -15,7 +15,6 @@ 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}; -use crate::util::VersionString; pub mod signer; @@ -25,7 +24,7 @@ pub fn version_api() -> ParentHandler { "add", from_fn_async(add_version) .with_metadata("admin", Value::Bool(true)) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_display() .with_call_remote::(), ) @@ -53,7 +52,8 @@ pub fn version_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddVersionParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, pub headline: String, pub release_notes: String, #[ts(type = "string")] @@ -99,7 +99,8 @@ pub async fn add_version( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RemoveVersionParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, } pub async fn remove_version( @@ -121,10 +122,10 @@ pub async fn remove_version( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] -pub struct GetVersionParams { +pub struct GetOsVersionParams { #[ts(type = "string | null")] #[arg(long = "src")] - pub source: Option, + pub source: Option, #[ts(type = "string | null")] #[arg(long = "target")] pub target: Option, @@ -138,13 +139,13 @@ pub struct GetVersionParams { pub async fn get_version( ctx: RegistryContext, - GetVersionParams { + GetOsVersionParams { source, target, server_id, arch, - }: GetVersionParams, -) -> Result, Error> { + }: GetOsVersionParams, +) -> Result, Error> { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { let created_at = Utc::now(); @@ -176,10 +177,7 @@ pub async fn get_version( .collect() } -pub fn display_version_info( - params: WithIoFormat, - info: BTreeMap, -) { +pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { use prettytable::*; if let Some(format) = params.format { @@ -197,7 +195,7 @@ pub fn display_version_info( ]); for (version, info) in &info { table.add_row(row![ - version.as_str(), + &version.to_string(), &info.headline, &info.release_notes, &info.iso.keys().into_iter().join(", "), diff --git a/core/startos/src/registry/os/version/signer.rs b/core/startos/src/registry/os/version/signer.rs index bb15860aa..51f7c6719 100644 --- a/core/startos/src/registry/os/version/signer.rs +++ b/core/startos/src/registry/os/version/signer.rs @@ -1,6 +1,7 @@ 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; @@ -12,7 +13,6 @@ use crate::registry::context::RegistryContext; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; -use crate::util::VersionString; pub fn signer_api() -> ParentHandler { ParentHandler::new() @@ -44,7 +44,8 @@ pub fn signer_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct VersionSignerParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, pub signer: Guid, } @@ -104,7 +105,8 @@ pub async fn remove_version_signer( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct ListVersionSignersParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, } pub async fn list_version_signers( diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 9bc772f78..c52f06ac0 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -11,13 +11,14 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::FullProgressTracker; +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; @@ -126,13 +127,16 @@ pub async fn cli_add_package( sign_phase.complete(); verify_phase.start(); - let mut src = S9pk::deserialize( - &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), - Some(&commitment), - ) - .await?; - src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) + 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(); @@ -140,7 +144,7 @@ pub async fn cli_add_package( &parent_method.into_iter().chain(method).join("."), imbl_value::json!({ "url": &url, - "signature": signature, + "signature": AnySignature::Ed25519(signature), "commitment": commitment, }), ) diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs index fb63be1bc..cae1289a9 100644 --- a/core/startos/src/registry/package/get.rs +++ b/core/startos/src/registry/package/get.rs @@ -21,6 +21,7 @@ use crate::util::VersionString; #[serde(rename_all = "camelCase")] #[ts(export)] pub enum PackageDetailLevel { + None, Short, Full, } @@ -50,7 +51,9 @@ pub struct GetPackageParams { #[arg(skip)] #[serde(rename = "__device_info")] pub device_info: Option, - pub other_versions: Option, + #[serde(default)] + #[arg(default_value = "none")] + pub other_versions: PackageDetailLevel, } #[derive(Debug, Deserialize, Serialize, TS)] @@ -126,7 +129,6 @@ fn get_matching_models<'a>( db: &'a Model, GetPackageParams { id, - version, source_version, device_info, .. @@ -148,22 +150,18 @@ fn get_matching_models<'a>( .into_iter() .map(|(v, info)| { Ok::<_, Error>( - if version + 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(true, |version| v.satisfies(version)) - && 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))? + .map_or(Ok(true), |device_info| info.works_for_device(device_info))? { Some((k.clone(), ExtendedVersion::from(v), info)) } else { @@ -187,24 +185,27 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu let mut other: BTreeMap>> = Default::default(); for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { - let mut package_best = best.remove(&id).unwrap_or_default(); - let mut package_other = other.remove(&id).unwrap_or_default(); - for worse_version in package_best - .keys() - .filter(|k| ***k < version) - .cloned() - .collect_vec() + 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)) { - if let Some(info) = package_best.remove(&worse_version) { - package_other.insert(worse_version, info); + 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); + } } - } - if package_best.keys().all(|k| !(**k > version)) { package_best.insert(version.into(), info); - } - best.insert(id.clone(), package_best); - if params.other_versions.is_some() { - other.insert(id.clone(), package_other); + } else { + package_other.insert(version.into(), info); } } if let Some(id) = params.id { @@ -224,12 +225,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?; let other = other.remove(&id).unwrap_or_default(); match params.other_versions { - None => to_value(&GetPackageResponse { + PackageDetailLevel::None => to_value(&GetPackageResponse { categories, best, other_versions: None, }), - Some(PackageDetailLevel::Short) => to_value(&GetPackageResponse { + PackageDetailLevel::Short => to_value(&GetPackageResponse { categories, best, other_versions: Some( @@ -239,7 +240,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?, ), }), - Some(PackageDetailLevel::Full) => to_value(&GetPackageResponseFull { + PackageDetailLevel::Full => to_value(&GetPackageResponseFull { categories, best, other_versions: other @@ -250,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu } } else { match params.other_versions { - None => to_value( + PackageDetailLevel::None => to_value( &best .into_iter() .map(|(id, best)| { @@ -276,7 +277,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Short) => to_value( + PackageDetailLevel::Short => to_value( &best .into_iter() .map(|(id, best)| { @@ -310,7 +311,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Full) => to_value( + PackageDetailLevel::Full => to_value( &best .into_iter() .map(|(id, best)| { @@ -354,7 +355,7 @@ pub fn display_package_info( } if let Some(_) = params.rest.id { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for table in from_value::(info)?.tables() { table.print_tty(false)?; println!(); @@ -366,7 +367,7 @@ pub fn display_package_info( } } } else { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for (_, package) in from_value::(info)? { for table in package.tables() { table.print_tty(false)?; diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 80055f06d..12a17f634 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use chrono::Utc; use exver::{Version, VersionRange}; use imbl_value::InternedString; use models::{DataUrl, PackageId, VersionString}; @@ -15,7 +16,7 @@ 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::{Description, HardwareRequirements}; +use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; @@ -49,12 +50,25 @@ pub struct Category { 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 { - pub title: String, + #[ts(type = "string")] + pub title: InternedString, pub icon: DataUrl<'static>, pub description: Description, pub release_notes: String, @@ -70,6 +84,10 @@ pub struct PackageVersionInfo { 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, @@ -80,6 +98,19 @@ pub struct PackageVersionInfo { 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?, @@ -91,10 +122,14 @@ impl PackageVersionInfo { 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: [( @@ -114,8 +149,11 @@ impl PackageVersionInfo { 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", &self.description.short]); - table.add_row(row![br -> "DESCRIPTION", &self.description.long]); + 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()]); diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs index ac09afbb1..cb2d317f9 100644 --- a/core/startos/src/registry/package/mod.rs +++ b/core/startos/src/registry/package/mod.rs @@ -16,14 +16,21 @@ pub fn package_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) - .subcommand("add", from_fn_async(add::add_package).no_cli()) + .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()) .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_call_remote::(), ) } diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/registry/signer/commitment/merkle_archive.rs index 0b61734b4..1b9d7d1e0 100644 --- a/core/startos/src/registry/signer/commitment/merkle_archive.rs +++ b/core/startos/src/registry/signer/commitment/merkle_archive.rs @@ -20,6 +20,35 @@ pub struct MerkleArchiveCommitment { #[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(dbg!(query).as_bytes()) { + match &*k { + "rootSighash" => { + root_sighash = Some(dbg!(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); diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs index ce60b7f88..e5bb776bf 100644 --- a/core/startos/src/registry/signer/commitment/request.rs +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -5,6 +5,7 @@ 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; @@ -37,8 +38,8 @@ impl RequestCommitment { .append_pair("size", &self.size.to_string()) .append_pair("blake3", &self.blake3.to_string()); } - pub fn from_query(url: &Url) -> Result { - let query: BTreeMap<_, _> = url.query_pairs().collect(); + 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()?, diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs index a0f095b7a..c9a2fd31b 100644 --- a/core/startos/src/s9pk/merkle_archive/expected.rs +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -1,4 +1,3 @@ - use std::ffi::OsStr; use std::path::Path; @@ -7,16 +6,16 @@ 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 +/// 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 { + pub fn new(dir: &'a DirectoryContents) -> Self { Self { keep: DirectoryContents::new(), - dir + dir, } } } @@ -42,22 +41,23 @@ impl<'a, T: Clone> Expected<'a, T> { 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 (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()) @@ -69,7 +69,7 @@ impl<'a, T: Clone> Expected<'a, T> { ), ErrorKind::ParseS9pk, )), - |acc, (name, _)| + |acc, (name, _)| if valid_extension(Path::new(&*name).extension()) { match acc { Ok(_) => Err(Error::new( @@ -96,8 +96,10 @@ impl<'a, T: Clone> Expected<'a, T> { pub struct Filter(DirectoryContents<()>); impl Filter { - pub fn keep_checked(&self, dir: &mut DirectoryContents) -> Result<(), Error> { + 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/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 977e5ebb2..3f30a4ce1 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -233,6 +233,10 @@ impl Entry { _ => 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), diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 6b7459787..cc9623ab6 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -1,3 +1,5 @@ +use std::cmp::min; +use std::io::SeekFrom; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; @@ -6,7 +8,7 @@ use blake3::Hash; use futures::future::BoxFuture; use futures::{Future, FutureExt}; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, Take}; use crate::prelude::*; use crate::s9pk::merkle_archive::hash::VerifyingWriter; @@ -17,8 +19,14 @@ 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, @@ -65,12 +73,16 @@ pub trait FileSource: Send + Sync + Sized + 'static { 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 } @@ -95,12 +107,16 @@ impl DynFileSource { } 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, @@ -123,6 +139,11 @@ impl FileSource for DynFileSource { 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, @@ -139,6 +160,13 @@ impl DynableFileSource for T { 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 } @@ -156,22 +184,34 @@ impl DynableFileSource for T { 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; @@ -272,12 +312,18 @@ pub struct Section { } 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 } @@ -342,12 +388,16 @@ impl From> for DynFileSource { 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, diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 83b78dad7..92f952077 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -15,14 +15,36 @@ 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()) + .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(()) + }, + ), + ) .subcommand("edit", edit()) .subcommand("inspect", inspect()) + .subcommand("convert", from_fn_async(convert).no_display()) } #[derive(Deserialize, Serialize, Parser)] @@ -193,3 +215,17 @@ async fn inspect_manifest( .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/v1/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs index 4a9956f9f..9b3eb9895 100644 --- a/core/startos/src/s9pk/v1/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -21,7 +21,7 @@ pub struct Manifest { #[serde(default)] pub git_hash: Option, pub title: String, - pub version: exver::emver::Version, + pub version: String, pub description: Description, #[serde(default)] pub assets: Assets, diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 914d2e5aa..8e62c69d0 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,8 +1,9 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; -use exver::ExtendedVersion; +use exver::{ExtendedVersion, VersionRange}; use models::ImageId; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; @@ -44,9 +45,9 @@ impl S9pk> { // manifest.json let manifest_raw = reader.manifest().await?; let manifest = from_value::(manifest_raw.clone())?; - let mut new_manifest = Manifest::from(manifest.clone()); + let mut new_manifest = Manifest::try_from(manifest.clone())?; - let images: BTreeMap = manifest + let images: BTreeSet<(ImageId, bool)> = manifest .package_procedures() .filter_map(|p| { if let PackageProcedure::Docker(p) = p { @@ -89,8 +90,6 @@ impl S9pk> { // images for arch in reader.docker_arches().await? { - let images_dir = tmp_dir.join("images").join(&arch); - tokio::fs::create_dir_all(&images_dir).await?; Command::new(CONTAINER_TOOL) .arg("load") .input(Some(&mut reader.docker_images(&arch).await?)) @@ -194,14 +193,22 @@ impl S9pk> { } } -impl From for Manifest { - fn from(value: ManifestV1) -> Self { +impl TryFrom for Manifest { + type Error = Error; + fn try_from(value: ManifestV1) -> Result { let default_url = value.upstream_repo.clone(); - Self { + Ok(Self { id: value.id, - title: value.title, - version: ExtendedVersion::from(value.version).into(), + 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, @@ -233,6 +240,7 @@ impl From for Manifest { DepInfo { description: value.description, optional: !value.requirement.required(), + s9pk: None, }, ) }) @@ -242,6 +250,6 @@ impl From for Manifest { git_hash: value.git_hash, os_version: value.eos_version, has_config: value.config.is_some(), - } + }) } } diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 9607bb654..1f24a0b73 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use color_eyre::eyre::eyre; -use exver::Version; +use exver::{Version, VersionRange}; use helpers::const_true; use imbl_value::InternedString; pub use models::PackageId; @@ -31,10 +31,16 @@ fn current_version() -> Version { #[ts(export)] pub struct Manifest { pub id: PackageId, - pub title: String, + #[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, @@ -81,6 +87,15 @@ impl Manifest { 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"))?; } @@ -148,8 +163,8 @@ impl Manifest { #[ts(export)] pub struct HardwareRequirements { #[serde(default)] - #[ts(type = "{ [key: string]: string }")] // TODO more specific key - pub device: BTreeMap, + #[ts(type = "{ display?: string, processor?: string }")] + pub device: BTreeMap, // TODO: array #[ts(type = "number | null")] pub ram: Option, #[ts(type = "string[] | null")] diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 2477e63a0..e012480af 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -6,10 +6,10 @@ 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::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ @@ -18,6 +18,7 @@ use crate::s9pk::merkle_archive::source::{ 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]; @@ -33,6 +34,10 @@ pub mod pack; ├── icon. ├── LICENSE.md ├── instructions.md + ├── dependencies + │ └── + │ ├── metadata.json + │ └── icon. ├── javascript.squashfs ├── assets │ └── .squashfs (xN) @@ -52,9 +57,10 @@ fn priority(s: &str) -> Option { a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1), "LICENSE.md" => Some(2), "instructions.md" => Some(3), - "javascript.squashfs" => Some(4), - "assets" => Some(5), - "images" => Some(6), + "dependencies" => Some(4), + "javascript.squashfs" => Some(5), + "assets" => Some(6), + "images" => Some(7), _ => None, } } @@ -101,22 +107,16 @@ impl S9pk { filter.keep_checked(self.archive.contents_mut()) } - pub async fn icon(&self) -> Result<(InternedString, FileContents), Error> { + 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, _)| { - Path::new(&*p) - .extension() - .and_then(|e| e.to_str()) - .and_then(mime) - .map_or(false, |e| e.starts_with("image/")) - }) - .filter_map(|(k, v)| v.into_file().map(|f| (k, f))) - { - let size = icon.size().await?; + 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))), @@ -134,7 +134,75 @@ impl S9pk { .and_then(|e| e.to_str()) .and_then(mime) .unwrap_or("image/png"); - DataUrl::from_reader(mime, contents.reader().await?, Some(contents.size().await?)).await + 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> { diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index ae6807c23..aa0fd39f2 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -1,6 +1,4 @@ use std::collections::BTreeSet; -use std::ffi::OsStr; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -10,25 +8,29 @@ use futures::{FutureExt, TryStreamExt}; use imbl_value::InternedString; use models::{ImageId, PackageId, VersionString}; use serde::{Deserialize, Serialize}; -use tokio::io::AsyncRead; 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, FileSource, TmpSource, + 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::Invoke; +use crate::util::serde::IoFormat; +use crate::util::{new_guid, Invoke, PathOrUrl}; #[cfg(not(feature = "docker"))] pub const CONTAINER_TOOL: &str = "podman"; @@ -58,14 +60,20 @@ impl SqfsDir { .get_or_try_init(|| async move { let guid = Guid::new(); let path = self.tmpdir.join(guid.as_ref()).with_extension("squashfs"); - let mut cmd = Command::new("mksquashfs"); if self.path.extension().and_then(|s| s.to_str()) == Some("tar") { - cmd.arg("-tar"); + Command::new("tar2sqfs") + .arg(&path) + .input(Some(&mut open_file(&self.path).await?)) + .invoke(ErrorKind::Filesystem) + .await?; + } else { + Command::new("mksquashfs") + .arg(&self.path) + .arg(&path) + .invoke(ErrorKind::Filesystem) + .await?; } - cmd.arg(&self.path) - .arg(&path) - .invoke(ErrorKind::Filesystem) - .await?; + Ok(MultiCursorFile::from( open_file(&path) .await @@ -83,7 +91,8 @@ pub enum PackSource { Squashfs(Arc), } impl FileSource for PackSource { - type Reader = Box; + type Reader = DynRead; + type SliceReader = DynRead; async fn size(&self) -> Result { match self { Self::Buffered(a) => Ok(a.len() as u64), @@ -102,11 +111,23 @@ impl FileSource for PackSource { } async fn reader(&self) -> Result { match self { - Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), - Self::File(f) => Ok(into_dyn_read(open_file(f).await?)), + 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 { @@ -150,24 +171,71 @@ impl PackParams { 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() == Some(OsStr::new("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().and_then(|s| s.to_str()) == Some("icon") { - Ok(path) - } else { - Err(e) - } + 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")), + ) }) - }}).await? + .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? } } - fn license(&self) -> PathBuf { - self.license - .as_ref() - .cloned() - .unwrap_or_else(|| self.path().join("LICENSE.md")) + 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 @@ -282,6 +350,15 @@ pub enum ImageSource { DockerTag(String), } impl ImageSource { + pub fn ingredients(&self) -> Vec { + match self { + Self::Packed => Vec::new(), + Self::DockerBuild { dockerfile, .. } => { + vec![dockerfile.clone().unwrap_or_else(|| "Dockerfile".into())] + } + Self::DockerTag(_) => Vec::new(), + } + } #[instrument(skip_all)] pub fn load<'a, S: From> + FileSource + Clone>( &'a self, @@ -320,7 +397,7 @@ impl ImageSource { format!("--platform=linux/{arch}") }; // docker buildx build ${path} -o type=image,name=start9/${id} - let tag = format!("start9/{id}/{image_id}:{version}"); + let tag = format!("start9/{id}/{image_id}:{}", new_guid()); Command::new(CONTAINER_TOOL) .arg("build") .arg(workdir) @@ -436,7 +513,7 @@ impl ImageSource { Command::new(CONTAINER_TOOL) .arg("export") .arg(container.trim()) - .pipe(Command::new("mksquashfs").arg("-").arg(&dest).arg("-tar")) + .pipe(Command::new("tar2sqfs").arg(&dest)) .capture(false) .invoke(ErrorKind::Docker) .await?; @@ -501,7 +578,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { "LICENSE.md".into(), Entry::file(TmpSource::new( tmp_dir.clone(), - PackSource::File(params.license()), + PackSource::File(params.license().await?), )), ); files.insert( @@ -541,6 +618,54 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { 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( @@ -555,3 +680,58 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { 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/service/cli.rs b/core/startos/src/service/cli.rs index f5e04999c..95add37fb 100644 --- a/core/startos/src/service/cli.rs +++ b/core/startos/src/service/cli.rs @@ -9,7 +9,7 @@ use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty}; use tokio::runtime::Runtime; use crate::lxc::HOST_RPC_SERVER_SOCKET; -use crate::service::service_effect_handler::EffectContext; +use crate::service::effects::context::EffectContext; #[derive(Debug, Default, Parser)] pub struct ContainerClientConfig { diff --git a/core/startos/src/service/dependencies.rs b/core/startos/src/service/dependencies.rs index 60fd76ad6..e8c6f07c4 100644 --- a/core/startos/src/service/dependencies.rs +++ b/core/startos/src/service/dependencies.rs @@ -38,7 +38,7 @@ impl ServiceActorSeed { ) .await .with_kind(ErrorKind::Dependency) - .map(|res| res.filter(|c| !c.is_empty())) + .map(|res| res.filter(|c| !c.is_empty() && Some(c) != remote_config.as_ref())) } } diff --git a/core/startos/src/service/effects/action.rs b/core/startos/src/service/effects/action.rs new file mode 100644 index 000000000..4719c6d3d --- /dev/null +++ b/core/startos/src/service/effects/action.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeMap; + +use models::{ActionId, PackageId}; + +use crate::action::ActionResult; +use crate::db::model::package::ActionMetadata; +use crate::rpc_continuations::Guid; +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ExportActionParams { + #[ts(optional)] + package_id: Option, + id: ActionId, + metadata: ActionMetadata, +} +pub async fn export_action(context: EffectContext, data: 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(data.id, data.metadata) + .map(|_| ()) + .unwrap_or_default(); + model.ser(&value) + }) + .await?; + Ok(()) +} + +pub async fn clear_actions(context: EffectContext) -> 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_actions_mut() + .ser(&BTreeMap::new()) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ExecuteAction { + #[serde(default)] + #[ts(skip)] + procedure_id: Guid, + #[ts(optional)] + package_id: Option, + action_id: ActionId, + #[ts(type = "any")] + input: Value, +} +pub async fn execute_action( + context: EffectContext, + ExecuteAction { + procedure_id, + package_id, + action_id, + input, + }: ExecuteAction, +) -> Result { + 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)? + .action(procedure_id, action_id, input) + .await + } else { + context.action(procedure_id, action_id, input).await + } +} diff --git a/core/startos/src/service/effects/callbacks.rs b/core/startos/src/service/effects/callbacks.rs new file mode 100644 index 000000000..1a9250aa8 --- /dev/null +++ b/core/startos/src/service/effects/callbacks.rs @@ -0,0 +1,311 @@ +use std::cmp::min; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::{Duration, SystemTime}; + +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 tracing::warn; + +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; +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>>, +} + +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() + }); + }) + } + + 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_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> { + 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() + } +} + +pub(super) fn clear_callbacks(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + context + .seed + .persistent_container + .state + .send_if_modified(|s| !std::mem::take(&mut s.callbacks).is_empty()); + context.seed.ctx.callbacks.gc(); + Ok(()) +} diff --git a/core/startos/src/service/effects/config.rs b/core/startos/src/service/effects/config.rs new file mode 100644 index 000000000..647d3e272 --- /dev/null +++ b/core/startos/src/service/effects/config.rs @@ -0,0 +1,53 @@ +use models::PackageId; + +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetConfiguredParams { + #[ts(optional)] + package_id: Option, +} +pub async fn get_configured(context: EffectContext) -> Result { + let context = context.deref()?; + let peeked = context.seed.ctx.db.peek().await; + let package_id = &context.seed.id; + peeked + .as_public() + .as_package_data() + .as_idx(package_id) + .or_not_found(package_id)? + .as_status() + .as_configured() + .de() +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetConfigured { + configured: bool, +} +pub async fn set_configured( + context: EffectContext, + SetConfigured { configured }: SetConfigured, +) -> 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_status_mut() + .as_configured_mut() + .ser(&configured) + }) + .await?; + 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..6b3c6f8a0 --- /dev/null +++ b/core/startos/src/service/effects/control.rs @@ -0,0 +1,66 @@ +use std::str::FromStr; + +use clap::builder::ValueParserFactory; + +use crate::service::effects::prelude::*; +use crate::util::clap::FromStrParser; + +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)] +#[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..26582d061 --- /dev/null +++ b/core/startos/src/service/effects/dependency.rs @@ -0,0 +1,392 @@ +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 itertools::Itertools; +use models::{HealthCheckId, PackageId, VersionString, VolumeId}; +use patch_db::json_ptr::JsonPointer; +use tokio::process::Command; + +use crate::db::model::package::{ + 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::rpc_continuations::Guid; +use crate::service::effects::prelude::*; +use crate::status::health_check::NamedHealthCheckResult; +use crate::util::clap::FromStrParser; +use crate::util::Invoke; +use crate::volume::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(&context.seed.ctx.datadir, &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> { + 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 { + #[serde(default)] + procedure_id: Guid, + dependencies: Vec, +} +pub async fn set_dependencies( + context: EffectContext, + SetDependenciesParams { + procedure_id, + 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 config_satisfied = + if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { + context + .dependency_config( + procedure_id.clone(), + dep_id.clone(), + dep_service.get_config(procedure_id.clone()).await?.config, + ) + .await? + .is_none() + } else { + true + }; + 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, + config_satisfied, + }; + 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()?; + + data.0 + .into_iter() + .map(|(id, current_dependency_info)| { + let CurrentDependencyInfo { + version_range, + kind, + .. + } = current_dependency_info; + Ok::<_, Error>(match kind { + CurrentDependencyKind::Exists => { + DependencyRequirement::Exists { id, version_range } + } + CurrentDependencyKind::Running { health_checks } => { + DependencyRequirement::Running { + id, + health_checks, + version_range, + } + } + }) + }) + .try_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, + #[ts(type = "string | null")] + installed_version: Option, + #[ts(type = "string[]")] + satisfies: BTreeSet, + is_running: bool, + config_satisfied: bool, + #[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 current_dependencies = db + .as_public() + .as_package_data() + .as_idx(&context.seed.id) + .or_not_found(&context.seed.id)? + .as_current_dependencies() + .de()?; + let package_ids: 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_ids.len()); + + for (package_id, dependency_info) in package_ids { + let title = dependency_info.title.clone(); + let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { + results.push(CheckDependenciesResult { + package_id, + title, + installed_version: None, + satisfies: BTreeSet::new(), + is_running: false, + config_satisfied: false, + 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()); + let is_installed = true; + let status = package.as_status().as_main().de()?; + let is_running = if is_installed { + status.running() + } else { + false + }; + let health_checks = status.health().cloned().unwrap_or_default(); + results.push(CheckDependenciesResult { + package_id, + title, + installed_version, + satisfies, + is_running, + config_satisfied: dependency_info.config_satisfied, + 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..9bf756d60 --- /dev/null +++ b/core/startos/src/service/effects/health.rs @@ -0,0 +1,46 @@ +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() + .as_main_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..e85481e96 --- /dev/null +++ b/core/startos/src/service/effects/mod.rs @@ -0,0 +1,212 @@ +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; +mod config; +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( + "execute-action", + from_fn_async(action::execute_action).no_cli(), + ) + .subcommand( + "export-action", + from_fn_async(action::export_action).no_cli(), + ) + .subcommand( + "clear-actions", + from_fn_async(action::clear_actions).no_cli(), + ) + // callbacks + .subcommand( + "clear-callbacks", + from_fn(callbacks::clear_callbacks).no_cli(), + ) + // config + .subcommand( + "get-configured", + from_fn_async(config::get_configured).no_cli(), + ) + .subcommand( + "set-configured", + from_fn_async(config::set_configured) + .no_display() + .with_call_remote::(), + ) + // 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::(), + ) + // 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-primary-url", + from_fn_async(net::host::get_primary_url).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..ba273323a --- /dev/null +++ b/core/startos/src/service/effects/net/bind.rs @@ -0,0 +1,56 @@ +use models::{HostId, PackageId}; + +use crate::net::host::binding::{BindOptions, LanInfo}; +use crate::net::host::HostKind; +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindParams { + kind: HostKind, + id: HostId, + internal_port: u16, + #[serde(flatten)] + options: BindOptions, +} +pub async fn bind( + context: EffectContext, + BindParams { + kind, + id, + internal_port, + options, + }: BindParams, +) -> Result<(), Error> { + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; + svc.bind(kind, id, internal_port, options).await +} + +pub async fn clear_bindings(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; + svc.clear_bindings().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: u32, +} +pub async fn get_service_port_forward( + context: EffectContext, + data: GetServicePortForwardParams, +) -> Result { + let internal_port = data.internal_port as u16; + + let context = context.deref()?; + let net_service = context.seed.persistent_container.net_service.lock().await; + net_service.get_lan_port(data.host_id, internal_port) +} 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..d320e7fe9 --- /dev/null +++ b/core/startos/src/service/effects/net/host.rs @@ -0,0 +1,73 @@ +use models::{HostId, PackageId}; + +use crate::net::host::address::HostAddress; +use crate::net::host::Host; +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 GetPrimaryUrlParams { + #[ts(optional)] + package_id: Option, + host_id: HostId, + #[ts(optional)] + callback: Option, +} +pub async fn get_primary_url( + context: EffectContext, + GetPrimaryUrlParams { + package_id, + host_id, + callback, + }: GetPrimaryUrlParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + Ok(None) // TODO +} + +#[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..c33a1a81e --- /dev/null +++ b/core/startos/src/service/effects/net/info.rs @@ -0,0 +1,9 @@ +use std::net::Ipv4Addr; + +use crate::service::effects::prelude::*; + +pub async fn get_container_ip(context: EffectContext) -> Result { + let context = context.deref()?; + let net_service = context.seed.persistent_container.net_service.lock().await; + Ok(net_service.get_ip()) +} 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..6cd4cd4c9 --- /dev/null +++ b/core/startos/src/service/effects/net/interface.rs @@ -0,0 +1,185 @@ +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, + has_primary: bool, + masked: bool, + address_info: AddressInfo, + r#type: ServiceInterfaceType, +} +pub async fn export_service_interface( + context: EffectContext, + ExportServiceInterfaceParams { + id, + name, + description, + has_primary, + 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, + has_primary, + 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) +} + +pub async fn clear_service_interfaces(context: EffectContext) -> 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() + .ser(&Default::default()) + }) + .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..d37a2d241 --- /dev/null +++ b/core/startos/src/service/effects/net/ssl.rs @@ -0,0 +1,169 @@ +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)| m.as_addresses().de()) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .map_ok(|a| InternedString::from_display(&a)) + .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)| m.as_addresses().de()) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .map_ok(|a| InternedString::from_display(&a)) + .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..6c12b425e --- /dev/null +++ b/core/startos/src/service/effects/store.rs @@ -0,0 +1,140 @@ +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 { + 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) + .or_not_found(&package_id)? + .de()?; + + 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) + .ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))? + .clone()) +} + +#[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..0375ef6c2 --- /dev/null +++ b/core/startos/src/service/effects/subcontainer/mod.rs @@ -0,0 +1,111 @@ +use std::path::{Path, PathBuf}; + +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::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.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, +} +#[instrument(skip_all)] +pub async fn create_subcontainer_fs( + context: EffectContext, + CreateSubcontainerFsParams { image_id }: 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 guard = OverlayGuard::mount(image, &mountpoint).await?; + 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(), guard); + 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..f31eb8f62 --- /dev/null +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -0,0 +1,424 @@ +use std::borrow::Cow; +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 unshare::Command as NSCommand; + +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(short = 'e', long = "env")] + env: Option, + #[arg(short = 'w', long = "workdir")] + workdir: Option, + #[arg(short = 'u', long = "user")] + 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 command = which::which_in( + command, + env.get("PATH") + .copied() + .map(Cow::Borrowed) + .or_else(|| std::env::var("PATH").ok().map(Cow::Owned)) + .as_deref(), + workdir.as_deref().unwrap_or(Path::new("/")), + ) + .with_kind(ErrorKind::Filesystem)?; + 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 { + env, + workdir, + user, + chroot, + command, + }: ExecParams, +) -> Result<(), Error> { + use unshare::{Namespace, Stdio}; + + use crate::service::cli::ContainerCliContext; + let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; + let mut cmd = NSCommand::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.unshare(&[Namespace::Pid, Namespace::Cgroup, Namespace::Ipc]); + 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(); + } + }); + if chroot.join("proc/1").exists() { + let ns_id = procfs::process::Process::new_with_root(chroot.join("proc")) + .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"))?; + } + let mut child = cmd + .spawn() + .map_err(color_eyre::eyre::Report::msg) + .with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; + let pid = child.pid(); + std::thread::spawn(move || { + for sig in sig.forever() { + nix::sys::signal::kill( + Pid::from_raw(pid), + Some(nix::sys::signal::Signal::try_from(sig).unwrap()), + ) + .unwrap(); + } + }); + 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() { + 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 { + env, + workdir, + user, + chroot, + command, + }: ExecParams, +) -> Result<(), Error> { + if std::io::stdin().is_terminal() { + 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/mod.rs b/core/startos/src/service/mod.rs index 581d2ac0d..5eae62756 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -10,6 +10,7 @@ use models::{HealthCheckId, PackageId, ProcedureName}; use persistent_container::PersistentContainer; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; +use service_actor::ServiceActor; use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; @@ -26,14 +27,11 @@ use crate::progress::{NamedProgress, Progress}; use crate::rpc_continuations::Guid; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; -use crate::service::transition::TransitionKind; -use crate::status::health_check::HealthCheckResult; -use crate::status::MainStatus; -use crate::util::actor::background::BackgroundJobQueue; +use crate::status::health_check::NamedHealthCheckResult; use crate::util::actor::concurrent::ConcurrentActor; -use crate::util::actor::Actor; use crate::util::io::create_file; -use crate::util::serde::Pem; +use crate::util::serde::{NoOutput, Pem}; +use crate::util::Never; use crate::volume::data_dir; mod action; @@ -41,12 +39,13 @@ pub mod cli; mod config; mod control; mod dependencies; +pub mod effects; pub mod persistent_container; mod properties; mod rpc; -pub mod service_effect_handler; +mod service_actor; pub mod service_map; -mod start_stop; +pub mod start_stop; mod transition; mod util; @@ -80,7 +79,7 @@ impl ServiceRef { ) -> Result<(), Error> { self.seed .persistent_container - .execute( + .execute::( Guid::new(), ProcedureName::Uninit, to_value(&target_version)?, @@ -90,10 +89,60 @@ impl ServiceRef { 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() { - ctx.db - .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) - .await?; + 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.lan + .assigned_port + .into_iter() + .chain(b.lan.assigned_ssl_port) + }), + ); + Ok(()) + })?; + 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(&ctx.datadir, &state.manifest.id, volume_id); + if tokio::fs::metadata(&path).await.is_ok() { + tokio::fs::remove_dir_all(&path).await?; + } + } + let logs_dir = ctx.datadir.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 = ctx + .datadir + .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(()) } @@ -187,10 +236,9 @@ impl Service { let ctx = ctx.clone(); move |s9pk: S9pk, i: Model| async move { for volume_id in &s9pk.as_manifest().volumes { - let tmp_path = - data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id); - if tokio::fs::metadata(&tmp_path).await.is_err() { - tokio::fs::create_dir_all(&tmp_path).await?; + let path = data_dir(&ctx.datadir, &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().as_main().de()?.running() { @@ -220,12 +268,13 @@ impl Service { tracing::error!("Error opening s9pk for install: {e}"); tracing::debug!("{e:?}") }) { - if let Ok(service) = Self::install(ctx.clone(), s9pk, None, None) - .await - .map_err(|e| { - tracing::error!("Error installing service: {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)); } @@ -257,6 +306,7 @@ impl Service { ctx.clone(), s9pk, Some(s.as_manifest().as_version().de()?), + None::, None, ) .await @@ -334,17 +384,39 @@ impl Service { pub async fn install( ctx: RpcContext, s9pk: S9pk, - src_version: Option, + 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(), + ); + } service .seed .persistent_container - .execute( + .execute::( Guid::new(), ProcedureName::Init, to_value(&src_version)?, @@ -382,26 +454,6 @@ impl Service { Ok(service) } - pub async fn restore( - ctx: RpcContext, - s9pk: S9pk, - backup_source: impl GenericMountGuard, - progress: Option, - ) -> Result { - let service = Service::install(ctx.clone(), s9pk, None, progress).await?; - - service - .actor - .send( - Guid::new(), - transition::restore::Restore { - path: backup_source.path().to_path_buf(), - }, - ) - .await??; - Ok(service) - } - #[instrument(skip_all)] pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { let id = &self.seed.id; @@ -417,10 +469,11 @@ impl Service { .send( Guid::new(), transition::backup::Backup { - path: guard.path().to_path_buf(), + path: guard.path().join("data"), }, ) - .await??; + .await?? + .await?; Ok(()) } @@ -440,7 +493,6 @@ impl Service { #[derive(Debug, Clone)] pub struct RunningStatus { - health: OrdMap, started: DateTime, } @@ -463,7 +515,6 @@ impl ServiceActorSeed { .running_status .take() .unwrap_or_else(|| RunningStatus { - health: Default::default(), started: Utc::now(), }), ); @@ -476,116 +527,6 @@ impl ServiceActorSeed { }); } } -#[derive(Clone)] -struct ServiceActor(Arc); - -impl Actor for ServiceActor { - fn init(&mut self, jobs: &BackgroundJobQueue) { - let seed = self.0.clone(); - jobs.add_job(async move { - let id = seed.id.clone(); - let mut current = seed.persistent_container.state.subscribe(); - - loop { - let kinds = current.borrow().kinds(); - - if let Err(e) = async { - let main_status = match ( - kinds.transition_state, - kinds.desired_state, - kinds.running_status, - ) { - (Some(TransitionKind::Restarting), StartStop::Stop, Some(_)) => { - seed.persistent_container.stop().await?; - MainStatus::Restarting - } - (Some(TransitionKind::Restarting), StartStop::Start, _) => { - seed.persistent_container.start().await?; - MainStatus::Restarting - } - (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, - (Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring, - (Some(TransitionKind::BackingUp), _, Some(status)) => { - MainStatus::BackingUp { - started: Some(status.started), - health: status.health.clone(), - } - } - (Some(TransitionKind::BackingUp), _, None) => MainStatus::BackingUp { - started: None, - health: OrdMap::new(), - }, - (None, StartStop::Stop, None) => MainStatus::Stopped, - (None, StartStop::Stop, Some(_)) => MainStatus::Stopping { - timeout: seed.persistent_container.stop().await?.into(), - }, - (None, StartStop::Start, Some(status)) => MainStatus::Running { - started: status.started, - health: status.health.clone(), - }, - (None, StartStop::Start, None) => { - seed.persistent_container.start().await?; - MainStatus::Starting - } - }; - 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().as_main().de()?; - let previous_health = previous.health(); - let previous_started = previous.started(); - let mut main_status = main_status; - match &mut main_status { - &mut MainStatus::Running { ref mut health, .. } - | &mut MainStatus::BackingUp { ref mut health, .. } => { - *health = previous_health.unwrap_or(health).clone(); - } - _ => (), - }; - match &mut main_status { - MainStatus::Running { - ref mut started, .. - } => { - *started = previous_started.unwrap_or(*started); - } - MainStatus::BackingUp { - ref mut started, .. - } => { - *started = previous_started.map(Some).unwrap_or(*started); - } - _ => (), - }; - i.as_status_mut().as_main_mut().ser(&main_status)?; - } - Ok(()) - }) - .await?; - - 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; - continue; - } - - seed.synchronized.notify_waiters(); - - tokio::select! { - _ = current.changed() => (), - } - } - }) - } -} #[derive(Deserialize, Serialize, Parser, TS)] pub struct ConnectParams { diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index e0b31ea97..dd7b5766d 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -1,11 +1,12 @@ -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; use futures::future::ready; use futures::{Future, FutureExt}; use helpers::NonDetachingJoinHandle; +use imbl::Vector; use models::{ImageId, ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; @@ -13,8 +14,6 @@ use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; -use super::service_effect_handler::{service_effect_handler, EffectContext}; -use super::transition::{TransitionKind, TransitionState}; use crate::context::RpcContext; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::idmapped::IdMapped; @@ -28,7 +27,11 @@ 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; @@ -40,8 +43,12 @@ 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) @@ -60,7 +67,9 @@ pub struct ServiceStateKinds { 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, @@ -89,7 +98,7 @@ pub struct PersistentContainer { volumes: BTreeMap, assets: BTreeMap, pub(super) images: BTreeMap>, - pub(super) overlays: Arc>>>>, + pub(super) subcontainers: Arc>>>>, pub(super) state: Arc>, pub(super) net_service: Mutex, destroyed: bool, @@ -161,17 +170,17 @@ impl PersistentContainer { .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( - &Bind::new( - asset_dir( - &ctx.datadir, - &s9pk.as_manifest().id, - &s9pk.as_manifest().version, - ) - .join(asset), - ), + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), mountpoint, MountType::ReadWrite, ) @@ -264,7 +273,7 @@ impl PersistentContainer { volumes, assets, images, - overlays: Arc::new(Mutex::new(BTreeMap::new())), + subcontainers: Arc::new(Mutex::new(BTreeMap::new())), state: Arc::new(watch::channel(ServiceState::new(start)).0), net_service: Mutex::new(net_service), destroyed: false, @@ -277,7 +286,7 @@ impl PersistentContainer { backup_path: impl AsRef, mount_type: MountType, ) -> Result { - let backup_path: PathBuf = backup_path.as_ref().to_path_buf(); + let backup_path = backup_path.as_ref(); let mountpoint = self .lxc_container .get() @@ -295,23 +304,20 @@ impl PersistentContainer { .arg(mountpoint.as_os_str()) .invoke(ErrorKind::Filesystem) .await?; - let bind = Bind::new(&backup_path); - let mount_guard = MountGuard::mount(&bind, &mountpoint, mount_type).await; + tokio::fs::create_dir_all(backup_path).await?; Command::new("chown") .arg("100000:100000") - .arg(backup_path.as_os_str()) + .arg(backup_path) .invoke(ErrorKind::Filesystem) .await?; - mount_guard + 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())), - service_effect_handler(), - ); + let server = Server::new(move || ready(Ok(socket_server_context.clone())), handler()); let path = self .lxc_container .get() @@ -366,6 +372,8 @@ impl PersistentContainer { self.rpc_client.request(rpc::Init, Empty {}).await?; + self.state.send_modify(|s| s.rt_initialized = true); + Ok(()) } @@ -380,42 +388,34 @@ impl PersistentContainer { 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 overlays = self.overlays.clone(); + let subcontainers = self.subcontainers.clone(); let lxc_container = self.lxc_container.take(); self.destroyed = true; - Some( - async move { - dbg!( - async move { - let mut errs = ErrorCollection::new(); - 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 *overlays.lock().await) { - errs.handle(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); - } - dbg!(errs.into_result()) - } - .await - ) + Some(async move { + let mut errs = ErrorCollection::new(); + 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)); } - .map(|a| dbg!(a)), - ) + 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.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)] @@ -430,22 +430,14 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn start(&self) -> Result<(), Error> { - self.execute( - Guid::new(), - ProcedureName::StartMain, - Value::Null, - Some(Duration::from_secs(5)), // TODO - ) - .await?; + self.rpc_client.request(rpc::Start, Empty {}).await?; Ok(()) } #[instrument(skip_all)] - pub async fn stop(&self) -> Result { - let timeout: Option = self - .execute(Guid::new(), ProcedureName::StopMain, Value::Null, None) - .await?; - Ok(timeout.map(|a| *a).unwrap_or(Duration::from_secs(30))) + pub async fn stop(&self) -> Result<(), Error> { + self.rpc_client.request(rpc::Stop, Empty {}).await?; + Ok(()) } #[instrument(skip_all)] @@ -480,6 +472,19 @@ impl PersistentContainer { .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, @@ -523,6 +528,12 @@ impl PersistentContainer { 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 { diff --git a/core/startos/src/service/rpc.rs b/core/startos/src/service/rpc.rs index eff44b2cf..25d8fb067 100644 --- a/core/startos/src/service/rpc.rs +++ b/core/startos/src/service/rpc.rs @@ -1,5 +1,8 @@ +use std::collections::BTreeSet; +use std::sync::{Arc, Weak}; use std::time::Duration; +use imbl::Vector; use imbl_value::Value; use models::ProcedureName; use rpc_toolkit::yajrc::RpcMethod; @@ -8,6 +11,8 @@ 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; @@ -27,6 +32,42 @@ impl serde::Serialize for Init { } } +#[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 { @@ -104,3 +145,74 @@ impl serde::Serialize for Sandbox { 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 { + let this = Arc::new(self); + let res = Arc::downgrade(&this); + container + .state + .send_if_modified(|s| s.callbacks.insert(this)); + CallbackHandle(res) + } +} + +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..0839afc0b --- /dev/null +++ b/core/startos/src/service/service_actor.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; +use std::time::Duration; + +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 { + 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().as_main().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().as_main_mut().ser(&main_status)?; + } + Ok(()) + }) + .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_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs deleted file mode 100644 index 7547584e0..000000000 --- a/core/startos/src/service/service_effect_handler.rs +++ /dev/null @@ -1,1463 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::ffi::OsString; -use std::net::Ipv4Addr; -use std::os::unix::process::CommandExt; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::{Arc, Weak}; - -use clap::builder::ValueParserFactory; -use clap::Parser; -use exver::VersionRange; -use imbl_value::json; -use itertools::Itertools; -use models::{ - ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, ServiceInterfaceId, VolumeId, -}; -use patch_db::json_ptr::JsonPointer; -use rpc_toolkit::{from_fn, from_fn_async, Context, Empty, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; -use tokio::process::Command; -use ts_rs::TS; -use url::Url; - -use crate::db::model::package::{ - ActionMetadata, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, - ManifestPreference, -}; -use crate::disk::mount::filesystem::overlayfs::OverlayGuard; -use crate::echo; -use crate::net::host::address::HostAddress; -use crate::net::host::binding::{BindOptions, LanInfo}; -use crate::net::host::{Host, HostKind}; -use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::s9pk::merkle_archive::source::http::HttpSource; -use crate::s9pk::rpc::SKIP_ENV; -use crate::s9pk::S9pk; -use crate::service::cli::ContainerCliContext; -use crate::service::Service; -use crate::status::health_check::HealthCheckResult; -use crate::status::MainStatus; -use crate::util::clap::FromStrParser; -use crate::util::Invoke; - -#[derive(Clone)] -pub(super) struct EffectContext(Weak); -impl EffectContext { - pub fn new(service: Weak) -> Self { - Self(service) - } -} -impl Context for EffectContext {} -impl EffectContext { - 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, - )) - } - } -} - -pub fn service_effect_handler() -> ParentHandler { - ParentHandler::new() - .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) - .subcommand( - "echo", - from_fn(echo::).with_call_remote::(), - ) - .subcommand( - "chroot", - from_fn(chroot::).no_display(), - ) - .subcommand("exists", from_fn_async(exists).no_cli()) - .subcommand("executeAction", from_fn_async(execute_action).no_cli()) - .subcommand("getConfigured", from_fn_async(get_configured).no_cli()) - .subcommand( - "stopped", - from_fn_async(stopped) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "running", - from_fn_async(running) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "restart", - from_fn_async(restart) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "shutdown", - from_fn_async(shutdown) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "setConfigured", - from_fn_async(set_configured) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "setMainStatus", - from_fn_async(set_main_status).with_call_remote::(), - ) - .subcommand("setHealth", from_fn_async(set_health).no_cli()) - .subcommand("getStore", from_fn_async(get_store).no_cli()) - .subcommand("setStore", from_fn_async(set_store).no_cli()) - .subcommand( - "exposeForDependents", - from_fn_async(expose_for_dependents).no_cli(), - ) - .subcommand( - "createOverlayedImage", - from_fn_async(create_overlayed_image) - .with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display()))) - .with_call_remote::(), - ) - .subcommand( - "destroyOverlayedImage", - from_fn_async(destroy_overlayed_image).no_cli(), - ) - .subcommand( - "getSslCertificate", - from_fn_async(get_ssl_certificate).no_cli(), - ) - .subcommand("getSslKey", from_fn_async(get_ssl_key).no_cli()) - .subcommand( - "getServiceInterface", - from_fn_async(get_service_interface).no_cli(), - ) - .subcommand("clearBindings", from_fn_async(clear_bindings).no_cli()) - .subcommand("bind", from_fn_async(bind).no_cli()) - .subcommand("getHostInfo", from_fn_async(get_host_info).no_cli()) - .subcommand( - "setDependencies", - from_fn_async(set_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "getDependencies", - from_fn_async(get_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "checkDependencies", - from_fn_async(check_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand("setSystemSmtp", from_fn_async(set_system_smtp).no_cli()) - .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) - .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) - .subcommand( - "getServicePortForward", - from_fn_async(get_service_port_forward).no_cli(), - ) - .subcommand( - "clearServiceInterfaces", - from_fn_async(clear_network_interfaces).no_cli(), - ) - .subcommand( - "exportServiceInterface", - from_fn_async(export_service_interface).no_cli(), - ) - .subcommand("getPrimaryUrl", from_fn_async(get_primary_url).no_cli()) - .subcommand( - "listServiceInterfaces", - from_fn_async(list_service_interfaces).no_cli(), - ) - .subcommand("removeAddress", from_fn_async(remove_address).no_cli()) - .subcommand("exportAction", from_fn_async(export_action).no_cli()) - .subcommand("removeAction", from_fn_async(remove_action).no_cli()) - .subcommand("mount", from_fn_async(mount).no_cli()) - - // TODO Callbacks -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetSystemSmtpParams { - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct SetSystemSmtpParams { - smtp: String, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetServicePortForwardParams { - #[ts(type = "string | null")] - package_id: Option, - internal_port: u32, - host_id: HostId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ExportServiceInterfaceParams { - id: ServiceInterfaceId, - name: String, - description: String, - has_primary: bool, - disabled: bool, - masked: bool, - address_info: AddressInfo, - r#type: ServiceInterfaceType, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetPrimaryUrlParams { - #[ts(type = "string | null")] - package_id: Option, - service_interface_id: ServiceInterfaceId, - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ListServiceInterfacesParams { - #[ts(type = "string | null")] - package_id: Option, - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct RemoveAddressParams { - id: ServiceInterfaceId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ExportActionParams { - #[ts(type = "string")] - id: ActionId, - metadata: ActionMetadata, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct RemoveActionParams { - #[ts(type = "string")] - id: ActionId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct MountTarget { - #[ts(type = "string")] - package_id: PackageId, - #[ts(type = "string")] - volume_id: VolumeId, - subpath: Option, - readonly: bool, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct MountParams { - location: String, - target: MountTarget, -} -async fn set_system_smtp(context: EffectContext, data: SetSystemSmtpParams) -> Result<(), Error> { - let context = context.deref()?; - context - .seed - .ctx - .db - .mutate(|db| { - let model = db.as_public_mut().as_server_info_mut().as_smtp_mut(); - model.ser(&mut Some(data.smtp)) - }) - .await -} -async fn get_system_smtp( - context: EffectContext, - data: GetSystemSmtpParams, -) -> Result { - let context = context.deref()?; - let res = context - .seed - .ctx - .db - .peek() - .await - .into_public() - .into_server_info() - .into_smtp() - .de()?; - - match res { - Some(smtp) => Ok(smtp), - None => Err(Error::new( - eyre!("SMTP not found"), - crate::ErrorKind::NotFound, - )), - } -} -async fn get_container_ip(context: EffectContext, _: Empty) -> Result { - let context = context.deref()?; - let net_service = context.seed.persistent_container.net_service.lock().await; - Ok(net_service.get_ip()) -} -async fn get_service_port_forward( - context: EffectContext, - data: GetServicePortForwardParams, -) -> Result { - let internal_port = data.internal_port as u16; - - let context = context.deref()?; - let net_service = context.seed.persistent_container.net_service.lock().await; - net_service.get_ext_port(data.host_id, internal_port) -} -async fn clear_network_interfaces(context: EffectContext, _: Empty) -> 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_service_interfaces_mut(); - let mut new_map = BTreeMap::new(); - model.ser(&mut new_map) - }) - .await -} -async fn export_service_interface( - context: EffectContext, - ExportServiceInterfaceParams { - id, - name, - description, - has_primary, - disabled, - 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, - has_primary, - disabled, - masked, - address_info, - interface_type: r#type, - }; - let svc_interface_with_host_info = service_interface; - - 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, &svc_interface_with_host_info)?; - Ok(()) - }) - .await?; - Ok(()) -} -async fn get_primary_url( - context: EffectContext, - GetPrimaryUrlParams { - package_id, - service_interface_id, - callback, - }: GetPrimaryUrlParams, -) -> Result, Error> { - let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - - Ok(None) // TODO -} -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()); - - context - .seed - .ctx - .db - .peek() - .await - .into_public() - .into_package_data() - .into_idx(&package_id) - .or_not_found(&package_id)? - .into_service_interfaces() - .de() -} -async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> 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_service_interfaces_mut(); - model.remove(&data.id) - }) - .await?; - Ok(()) -} -async fn export_action(context: EffectContext, data: 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(data.id, data.metadata) - .map(|_| ()) - .unwrap_or_default(); - model.ser(&value) - }) - .await?; - Ok(()) -} -async fn remove_action(context: EffectContext, data: RemoveActionParams) -> 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.remove(&data.id).map(|_| ()).unwrap_or_default(); - model.ser(&value) - }) - .await?; - Ok(()) -} -async fn mount(context: EffectContext, data: MountParams) -> Result { - // TODO - todo!() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -struct Callback(#[ts(type = "() => void")] i64); - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetHostInfoParams { - host_id: HostId, - #[ts(type = "string | null")] - package_id: Option, - callback: Callback, -} -async fn get_host_info( - context: EffectContext, - GetHostInfoParams { - callback, - package_id, - host_id, - }: GetHostInfoParams, -) -> Result { - let context = context.deref()?; - let db = context.seed.ctx.db.peek().await; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - - db.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)? - .de() -} - -async fn clear_bindings(context: EffectContext, _: Empty) -> Result<(), Error> { - let context = context.deref()?; - let mut svc = context.seed.persistent_container.net_service.lock().await; - svc.clear_bindings().await?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct BindParams { - kind: HostKind, - id: HostId, - internal_port: u16, - #[serde(flatten)] - options: BindOptions, -} -async fn bind(context: EffectContext, bind_params: Value) -> Result<(), Error> { - let BindParams { - kind, - id, - internal_port, - options, - } = from_value(bind_params)?; - let context = context.deref()?; - let mut svc = context.seed.persistent_container.net_service.lock().await; - svc.bind(kind, id, internal_port, options).await -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetServiceInterfaceParams { - #[ts(type = "string | null")] - package_id: Option, - service_interface_id: ServiceInterfaceId, - callback: Callback, -} - -async fn get_service_interface( - context: EffectContext, - GetServiceInterfaceParams { - callback, - package_id, - service_interface_id, - }: GetServiceInterfaceParams, -) -> Result { - 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) - .or_not_found(&package_id)? - .as_service_interfaces() - .as_idx(&service_interface_id) - .or_not_found(&service_interface_id)? - .de()?; - Ok(interface) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ChrootParams { - #[arg(short = 'e', long = "env")] - env: Option, - #[arg(short = 'w', long = "workdir")] - workdir: Option, - #[arg(short = 'u', long = "user")] - user: Option, - path: PathBuf, - #[ts(type = "string")] - command: OsString, - #[ts(type = "string[]")] - args: Vec, -} -fn chroot( - _: C, - ChrootParams { - env, - workdir, - user, - path, - command, - args, - }: ChrootParams, -) -> Result<(), Error> { - let mut cmd = std::process::Command::new(command); - if let Some(env) = env { - for (k, v) in std::fs::read_to_string(env)? - .lines() - .map(|l| l.trim()) - .filter_map(|l| l.split_once("=")) - .filter(|(k, _)| !SKIP_ENV.contains(&k)) - { - cmd.env(k, v); - } - } - nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted - std::os::unix::fs::chroot(path)?; - 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")? - .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); - } - cmd.args(args); - Err(cmd.exec().into()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -enum Algorithm { - Ecdsa, - Ed25519, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetSslCertificateParams { - package_id: Option, - host_id: String, - algorithm: Option, //"ecdsa" | "ed25519" -} - -async fn get_ssl_certificate( - context: EffectContext, - GetSslCertificateParams { - package_id, - algorithm, - host_id, - }: GetSslCertificateParams, -) -> Result { - // TODO - let fake = include_str!("./fake.cert.pem"); - Ok(json!([fake, fake, fake])) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetSslKeyParams { - package_id: Option, - host_id: String, - algorithm: Option, -} - -async fn get_ssl_key( - context: EffectContext, - GetSslKeyParams { - package_id, - host_id, - algorithm, - }: GetSslKeyParams, -) -> Result { - // TODO - let fake = include_str!("./fake.cert.key"); - Ok(json!(fake)) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetStoreParams { - #[ts(type = "string | null")] - package_id: Option, - #[ts(type = "string")] - path: JsonPointer, -} - -async fn get_store( - context: EffectContext, - GetStoreParams { package_id, path }: GetStoreParams, -) -> Result { - 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) - .or_not_found(&package_id)? - .de()?; - - Ok(path - .get(&value) - .ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))? - .clone()) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct SetStoreParams { - #[ts(type = "any")] - value: Value, - #[ts(type = "string")] - path: JsonPointer, -} - -async fn set_store( - context: EffectContext, - SetStoreParams { value, path }: SetStoreParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - 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?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ExposeForDependentsParams { - #[ts(type = "string[]")] - paths: Vec, -} - -async fn expose_for_dependents( - context: EffectContext, - ExposeForDependentsParams { paths }: ExposeForDependentsParams, -) -> Result<(), Error> { - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ParamsPackageId { - #[ts(type = "string")] - package_id: PackageId, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct ParamsMaybePackageId { - #[ts(type = "string | null")] - package_id: Option, -} - -async fn exists(context: EffectContext, params: ParamsPackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package = peeked - .as_public() - .as_package_data() - .as_idx(¶ms.package_id) - .is_some(); - Ok(json!(package)) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ExecuteAction { - #[serde(default)] - procedure_id: Guid, - #[ts(type = "string | null")] - service_id: Option, - #[ts(type = "string")] - action_id: ActionId, - #[ts(type = "any")] - input: Value, -} -async fn execute_action( - context: EffectContext, - ExecuteAction { - procedure_id, - service_id, - action_id, - input, - }: ExecuteAction, -) -> Result { - let context = context.deref()?; - let package_id = service_id - .clone() - .unwrap_or_else(|| context.seed.id.clone()); - - Ok(json!(context.action(procedure_id, action_id, input).await?)) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct FromService {} -async fn get_configured(context: EffectContext, _: Empty) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = &context.seed.id; - let package = peeked - .as_public() - .as_package_data() - .as_idx(package_id) - .or_not_found(package_id)? - .as_status() - .as_configured() - .de()?; - Ok(json!(package)) -} - -async fn stopped(context: EffectContext, params: ParamsMaybePackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = params.package_id.unwrap_or_else(|| context.seed.id.clone()); - let package = peeked - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_status() - .as_main() - .de()?; - Ok(json!(matches!(package, MainStatus::Stopped))) -} -async fn running(context: EffectContext, params: ParamsPackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = params.package_id; - let package = peeked - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_status() - .as_main() - .de()?; - Ok(json!(matches!(package, MainStatus::Running { .. }))) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ProcedureId { - #[serde(default)] - #[arg(default_value_t, long)] - procedure_id: Guid, -} - -async fn restart( - context: EffectContext, - ProcedureId { procedure_id }: ProcedureId, -) -> Result<(), Error> { - let context = context.deref()?; - context.restart(procedure_id).await?; - Ok(()) -} - -async fn shutdown( - context: EffectContext, - ProcedureId { procedure_id }: ProcedureId, -) -> Result<(), Error> { - let context = context.deref()?; - context.stop(procedure_id).await?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct SetConfigured { - configured: bool, -} -async fn set_configured(context: EffectContext, params: SetConfigured) -> Result { - 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_status_mut() - .as_configured_mut() - .ser(¶ms.configured) - }) - .await?; - Ok(json!(())) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -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, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct SetMainStatus { - status: SetMainStatusStatus, -} -async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Result { - let context = context.deref()?; - match params.status { - SetMainStatusStatus::Running => context.seed.started(), - SetMainStatusStatus::Stopped => context.seed.stopped(), - } - Ok(Value::Null) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct SetHealth { - id: HealthCheckId, - #[serde(flatten)] - result: HealthCheckResult, -} - -async fn set_health( - context: EffectContext, - SetHealth { id, result }: SetHealth, -) -> Result { - 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() - .as_main_mut() - .mutate(|main| { - match main { - &mut MainStatus::Running { ref mut health, .. } - | &mut MainStatus::BackingUp { ref mut health, .. } => { - health.insert(id, result); - } - _ => (), - } - Ok(()) - }) - }) - .await?; - Ok(json!(())) -} -#[derive(serde::Deserialize, serde::Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -pub struct DestroyOverlayedImageParams { - guid: Guid, -} - -#[instrument(skip_all)] -pub async fn destroy_overlayed_image( - context: EffectContext, - DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, -) -> Result<(), Error> { - let context = context.deref()?; - if context - .seed - .persistent_container - .overlays - .lock() - .await - .remove(&guid) - .is_none() - { - tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping"); - } - Ok(()) -} -#[derive(serde::Deserialize, serde::Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -pub struct CreateOverlayedImageParams { - image_id: ImageId, -} - -#[instrument(skip_all)] -pub async fn create_overlayed_image( - context: EffectContext, - CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams, -) -> 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/overlays") - .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 guard = OverlayGuard::mount(image, &mountpoint).await?; - Command::new("chown") - .arg("100000:100000") - .arg(&mountpoint) - .invoke(ErrorKind::Filesystem) - .await?; - tracing::info!("Mounted overlay {guid} for {image_id}"); - context - .seed - .persistent_container - .overlays - .lock() - .await - .insert(guid.clone(), guard); - Ok((container_mountpoint, guid)) - } else { - Err(Error::new( - eyre!("image {image_id} not found in s9pk"), - ErrorKind::NotFound, - )) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -enum DependencyKind { - Exists, - Running, -} - -#[derive(Debug, Clone, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase", tag = "kind")] -#[ts(export)] -enum DependencyRequirement { - #[serde(rename_all = "camelCase")] - Running { - #[ts(type = "string")] - id: PackageId, - #[ts(type = "string[]")] - health_checks: BTreeSet, - #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, - }, - #[serde(rename_all = "camelCase")] - Exists { - #[ts(type = "string")] - id: PackageId, - #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, - }, -} -// 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()?, - registry_url: "".parse()?, // TODO - version_spec: "*".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, - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO - }) - } - None => Ok(Self::Running { - id: s.parse()?, - health_checks: BTreeSet::new(), - registry_url: "".parse()?, // TODO - version_spec: "*".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)] -struct SetDependenciesParams { - #[serde(default)] - procedure_id: Guid, - dependencies: Vec, -} - -async fn set_dependencies( - context: EffectContext, - SetDependenciesParams { - procedure_id, - 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, registry_url, version_spec) = match dependency { - DependencyRequirement::Exists { - id, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyKind::Exists, - registry_url, - version_spec, - ), - DependencyRequirement::Running { - id, - health_checks, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyKind::Running { health_checks }, - registry_url, - version_spec, - ), - }; - let (icon, title) = match async { - let remote_s9pk = S9pk::deserialize( - &Arc::new( - HttpSource::new( - context.seed.ctx.client.clone(), - registry_url - .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, - ) - .await?, - ), - None, // TODO - ) - .await?; - - let icon = remote_s9pk.icon_data_url().await?; - - Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone())) - } - .await - { - Ok(a) => a, - Err(e) => { - tracing::error!("Error fetching remote s9pk: {e}"); - tracing::debug!("{e:?}"); - ( - DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")), - dep_id.to_string(), - ) - } - }; - let config_satisfied = - if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { - context - .dependency_config( - procedure_id.clone(), - dep_id.clone(), - dep_service.get_config(procedure_id.clone()).await?.config, - ) - .await? - .is_none() - } else { - true - }; - deps.insert( - dep_id, - CurrentDependencyInfo { - kind, - registry_url, - version_spec, - icon, - title, - config_satisfied, - }, - ); - } - 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 -} - -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()?; - - data.0 - .into_iter() - .map(|(id, current_dependency_info)| { - let CurrentDependencyInfo { - registry_url, - version_spec, - kind, - .. - } = current_dependency_info; - Ok::<_, Error>(match kind { - CurrentDependencyKind::Exists => DependencyRequirement::Exists { - id, - registry_url, - version_spec, - }, - CurrentDependencyKind::Running { health_checks } => { - DependencyRequirement::Running { - id, - health_checks, - version_spec, - registry_url, - } - } - }) - }) - .try_collect() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct CheckDependenciesParam { - package_ids: Option>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct CheckDependenciesResult { - package_id: PackageId, - is_installed: bool, - is_running: bool, - health_checks: Vec, - #[ts(type = "string | null")] - version: Option, -} - -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 current_dependencies = db - .as_public() - .as_package_data() - .as_idx(&context.seed.id) - .or_not_found(&context.seed.id)? - .as_current_dependencies() - .de()?; - let package_ids: 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_ids.len()); - - for (package_id, dependency_info) in package_ids { - let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { - results.push(CheckDependenciesResult { - package_id, - is_installed: false, - is_running: false, - health_checks: vec![], - version: None, - }); - continue; - }; - let installed_version = package - .as_state_info() - .as_manifest(ManifestPreference::New) - .as_version() - .de()? - .into_version(); - let version = Some(installed_version.clone()); - if !installed_version.satisfies(&dependency_info.version_spec) { - results.push(CheckDependenciesResult { - package_id, - is_installed: false, - is_running: false, - health_checks: vec![], - version, - }); - continue; - } - let is_installed = true; - let status = package.as_status().as_main().de()?; - let is_running = if is_installed { - status.running() - } else { - false - }; - let health_checks = status - .health() - .cloned() - .unwrap_or_default() - .into_iter() - .map(|(_, val)| val) - .collect(); - results.push(CheckDependenciesResult { - package_id, - is_installed, - is_running, - health_checks, - version, - }); - } - Ok(results) -} diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index af10a065c..0e6a959ae 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -173,6 +173,7 @@ impl ServiceMap { } else { PackageState::Installing(installing) }, + data_version: None, status: Status { configured: false, main: MainStatus::Stopped, @@ -265,35 +266,20 @@ impl ServiceMap { } else { None }; - if let Some(recovery_source) = recovery_source { - *service = Some( - Service::restore( - ctx, - s9pk, - recovery_source, - Some(InstallProgressHandles { - finalization_progress, - progress, - }), - ) - .await? - .into(), - ); - } else { - *service = Some( - Service::install( - ctx, - s9pk, - prev, - Some(InstallProgressHandles { - finalization_progress, - progress, - }), - ) - .await? - .into(), - ); - } + *service = Some( + Service::install( + ctx, + s9pk, + prev, + recovery_source, + Some(InstallProgressHandles { + finalization_progress, + progress, + }), + ) + .await? + .into(), + ); drop(service); sync_progress_task.await.map_err(|_| { diff --git a/core/startos/src/service/start_stop.rs b/core/startos/src/service/start_stop.rs index 178176023..64d4022d6 100644 --- a/core/startos/src/service/start_stop.rs +++ b/core/startos/src/service/start_stop.rs @@ -1,6 +1,10 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + use crate::status::MainStatus; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] pub enum StartStop { Start, Stop, @@ -11,23 +15,19 @@ impl StartStop { 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 { started, health: _ } if started.is_some() => StartStop::Start, - MainStatus::BackingUp { - started: _, - health: _, - } => StartStop::Stop, - } - } -} +// 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 index f7591f0d9..d8606f534 100644 --- a/core/startos/src/service/transition/backup.rs +++ b/core/startos/src/service/transition/backup.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use std::sync::Arc; +use futures::future::BoxFuture; use futures::FutureExt; use models::ProcedureName; @@ -19,7 +21,7 @@ pub(in crate::service) struct Backup { pub path: PathBuf, } impl Handler for ServiceActor { - type Response = Result<(), Error>; + type Response = Result>, Error>; fn conflicts_with(_: &Backup) -> ConflictBuilder { ConflictBuilder::everything() .except::() @@ -37,43 +39,31 @@ impl Handler for ServiceActor { let path = backup.path.clone(); let seed = self.0.clone(); - let state = self.0.persistent_container.state.clone(); - let transition = RemoteCancellable::new( - async move { - temp.stop(); + 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_none()) + .wait_for(|s| s.running_status.is_some()) .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); - state.send_modify(|s| { - s.transition_state.take(); - }); - Ok::<_, Error>(()) } - .map(|x| { - if let Err(err) = dbg!(x) { - tracing::debug!("{:?}", err); - tracing::warn!("{}", err); - } - }), - ); + drop(temp); + Ok::<_, Arc>(()) + }); let cancel_handle = transition.cancellation_handle(); let transition = transition.shared(); let job_transition = transition.clone(); @@ -92,9 +82,11 @@ impl Handler for ServiceActor { if let Some(t) = old { t.abort().await; } - match transition.await { - None => Err(Error::new(eyre!("Backup canceled"), ErrorKind::Unknown)), - Some(x) => Ok(x), - } + 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 index 7b7f10f2a..a6a41073b 100644 --- a/core/startos/src/service/transition/mod.rs +++ b/core/startos/src/service/transition/mod.rs @@ -79,7 +79,10 @@ impl TempDesiredRestore { } impl Drop for TempDesiredRestore { fn drop(&mut self) { - self.0.send_modify(|s| s.temp_desired_state = None); + self.0.send_modify(|s| { + s.temp_desired_state.take(); + s.transition_state.take(); + }); } } // impl Deref for TempDesiredState { 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/status/health_check.rs b/core/startos/src/status/health_check.rs index 90b20f8c5..1b1e2a7b6 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -9,25 +9,25 @@ use crate::util::clap::FromStrParser; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] -pub struct HealthCheckResult { +pub struct NamedHealthCheckResult { pub name: String, #[serde(flatten)] - pub kind: HealthCheckResultKind, + pub kind: NamedHealthCheckResultKind, } // healthCheckName:kind:message OR healthCheckName:kind -impl FromStr for HealthCheckResult { +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" => HealthCheckResultKind::Success { message }, - "disabled" => HealthCheckResultKind::Disabled { message }, - "starting" => HealthCheckResultKind::Starting { message }, - "loading" => HealthCheckResultKind::Loading { + "success" => NamedHealthCheckResultKind::Success { message }, + "disabled" => NamedHealthCheckResultKind::Disabled { message }, + "starting" => NamedHealthCheckResultKind::Starting { message }, + "loading" => NamedHealthCheckResultKind::Loading { message: message.unwrap_or_default(), }, - "failure" => HealthCheckResultKind::Failure { + "failure" => NamedHealthCheckResultKind::Failure { message: message.unwrap_or_default(), }, _ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")), @@ -47,7 +47,7 @@ impl FromStr for HealthCheckResult { } } } -impl ValueParserFactory for HealthCheckResult { +impl ValueParserFactory for NamedHealthCheckResult { type Parser = FromStrParser; fn value_parser() -> Self::Parser { FromStrParser::new() @@ -57,40 +57,44 @@ impl ValueParserFactory for HealthCheckResult { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] #[serde(tag = "result")] -pub enum HealthCheckResultKind { +pub enum NamedHealthCheckResultKind { Success { message: Option }, Disabled { message: Option }, Starting { message: Option }, Loading { message: 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 { let name = &self.name; match &self.kind { - HealthCheckResultKind::Success { message } => { + NamedHealthCheckResultKind::Success { message } => { if let Some(message) = message { write!(f, "{name}: Succeeded ({message})") } else { write!(f, "{name}: Succeeded") } } - HealthCheckResultKind::Disabled { message } => { + NamedHealthCheckResultKind::Disabled { message } => { if let Some(message) = message { write!(f, "{name}: Disabled ({message})") } else { write!(f, "{name}: Disabled") } } - HealthCheckResultKind::Starting { message } => { + NamedHealthCheckResultKind::Starting { message } => { if let Some(message) = message { write!(f, "{name}: Starting ({message})") } else { write!(f, "{name}: Starting") } } - HealthCheckResultKind::Loading { message } => write!(f, "{name}: Loading ({message})"), - HealthCheckResultKind::Failure { message } => write!(f, "{name}: Failed ({message})"), + 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 520fe5089..c10a7b89f 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -7,7 +7,8 @@ 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, TS)] @@ -22,28 +23,24 @@ pub struct Status { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(tag = "status")] #[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] pub enum MainStatus { Stopped, Restarting, Restoring, - #[serde(rename_all = "camelCase")] - Stopping { - timeout: crate::util::serde::Duration, + Stopping, + Starting { + #[ts(as = "BTreeMap")] + health: OrdMap, }, - Starting, - #[serde(rename_all = "camelCase")] Running { #[ts(type = "string")] started: DateTime, - #[ts(as = "BTreeMap")] - health: OrdMap, + #[ts(as = "BTreeMap")] + health: OrdMap, }, - #[serde(rename_all = "camelCase")] BackingUp { - #[ts(type = "string | null")] - started: Option>, - #[ts(as = "BTreeMap")] - health: OrdMap, + on_complete: StartStop, }, } impl MainStatus { @@ -51,60 +48,37 @@ impl MainStatus { match self { MainStatus::Starting { .. } | MainStatus::Running { .. } + | MainStatus::Restarting | MainStatus::BackingUp { - started: Some(_), .. + on_complete: StartStop::Start, } => true, MainStatus::Stopped | MainStatus::Restoring | MainStatus::Stopping { .. } - | MainStatus::Restarting - | MainStatus::BackingUp { started: None, .. } => false, + | MainStatus::BackingUp { + on_complete: StartStop::Stop, + } => false, } } - // pub fn stop(&mut self) { - // match self { - // MainStatus::Starting { .. } | MainStatus::Running { .. } => { - // *self = MainStatus::Stopping; - // } - // MainStatus::BackingUp { started, .. } => { - // *started = None; - // } - // MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => (), - // } - // } - pub fn started(&self) -> Option> { - match self { - MainStatus::Running { started, .. } => Some(*started), - MainStatus::BackingUp { started, .. } => *started, - MainStatus::Stopped => None, - MainStatus::Restoring => None, - MainStatus::Restarting => None, - MainStatus::Stopping { .. } => None, - MainStatus::Starting { .. } => None, + + pub fn backing_up(self) -> Self { + MainStatus::BackingUp { + on_complete: if self.running() { + StartStop::Start + } else { + StartStop::Stop + }, } } - 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::Restoring - | MainStatus::Restarting => (None, Default::default()), - MainStatus::BackingUp { .. } => return self.clone(), - }; - MainStatus::BackingUp { started, health } - } - pub fn health(&self) -> Option<&OrdMap> { + pub fn health(&self) -> Option<&OrdMap> { match self { - MainStatus::Running { health, .. } => Some(health), - MainStatus::BackingUp { health, .. } => Some(health), - MainStatus::Stopped + MainStatus::Running { health, .. } | MainStatus::Starting { health } => Some(health), + MainStatus::BackingUp { .. } + | MainStatus::Stopped | MainStatus::Restoring | MainStatus::Stopping { .. } | MainStatus::Restarting => None, - MainStatus::Starting { .. } => None, } } } diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 7a8cb9afe..7af94588b 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -5,6 +5,7 @@ use chrono::Utc; use clap::Parser; use color_eyre::eyre::eyre; use futures::FutureExt; +use imbl::vector; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::process::Command; @@ -824,6 +825,51 @@ 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(()) +} + #[tokio::test] #[ignore] pub async fn test_get_temp() { diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 79d852754..51d8d77ae 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -18,6 +18,12 @@ use tracing::instrument; 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::efivarfs::{self, 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::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; @@ -247,6 +253,7 @@ async fn maybe_do_update( 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)); @@ -300,6 +307,7 @@ async fn maybe_do_update( asset, UpdateProgressHandles { progress, + prune_phase, download_phase, reverify_phase, sync_boot_phase, @@ -369,6 +377,7 @@ async fn maybe_do_update( struct UpdateProgressHandles { progress: FullProgressTracker, + prune_phase: PhaseProgressTrackerHandle, download_phase: PhaseProgressTrackerHandle, reverify_phase: PhaseProgressTrackerHandle, sync_boot_phase: PhaseProgressTrackerHandle, @@ -381,12 +390,20 @@ async fn do_update( asset: RegistryAsset, UpdateProgressHandles { progress, + mut prune_phase, mut download_phase, mut reverify_phase, mut sync_boot_phase, mut finalize_phase, }: UpdateProgressHandles, ) -> Result<(), Error> { + prune_phase.start(); + Command::new("/usr/lib/startos/scripts/prune-images") + .arg(asset.commitment.size.to_string()) + .invoke(ErrorKind::Filesystem) + .await?; + prune_phase.complete(); + download_phase.start(); let path = Path::new("/media/startos/images") .join(hex::encode(&asset.commitment.hash[..16])) @@ -420,6 +437,72 @@ async fn do_update( .arg("boot") .invoke(crate::ErrorKind::Filesystem) .await?; + if &*PLATFORM != "raspberrypi" { + let mountpoint = "/media/startos/next"; + let root_guard = OverlayGuard::mount( + TmpMountGuard::mount(&BlockDev::new(&path), MountType::ReadOnly).await?, + mountpoint, + ) + .await?; + let startos = MountGuard::mount( + &Bind::new("/media/startos/root"), + root_guard.path().join("media/startos/root"), + MountType::ReadOnly, + ) + .await?; + let boot_guard = MountGuard::mount( + &Bind::new("/boot"), + root_guard.path().join("boot"), + MountType::ReadWrite, + ) + .await?; + let dev = MountGuard::mount( + &Bind::new("/dev"), + root_guard.path().join("dev"), + MountType::ReadWrite, + ) + .await?; + let proc = MountGuard::mount( + &Bind::new("/proc"), + root_guard.path().join("proc"), + MountType::ReadWrite, + ) + .await?; + let sys = MountGuard::mount( + &Bind::new("/sys"), + root_guard.path().join("sys"), + MountType::ReadWrite, + ) + .await?; + let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { + Some( + MountGuard::mount( + &EfiVarFs, + root_guard.path().join("sys/firmware/efi/efivars"), + MountType::ReadWrite, + ) + .await?, + ) + } else { + None + }; + + Command::new("chroot") + .arg(root_guard.path()) + .arg("update-grub2") + .invoke(ErrorKind::Grub) + .await?; + + if let Some(efivarfs) = efivarfs { + efivarfs.unmount(false).await?; + } + sys.unmount(false).await?; + proc.unmount(false).await?; + dev.unmount(false).await?; + boot_guard.unmount(false).await?; + startos.unmount(false).await?; + root_guard.unmount(false).await?; + } sync_boot_phase.complete(); finalize_phase.start(); @@ -429,6 +512,7 @@ async fn do_update( .arg("/media/startos/config/current.rootfs") .invoke(crate::ErrorKind::Filesystem) .await?; + Command::new("sync").invoke(ErrorKind::Filesystem).await?; finalize_phase.complete(); progress.complete(); diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index 4735a63fb..20f294400 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -5,10 +5,12 @@ use std::task::Poll; use std::time::Duration; use axum::body::Body; +use axum::extract::Request; use axum::response::Response; -use futures::{ready, FutureExt, StreamExt}; +use bytes::Bytes; +use futures::{ready, FutureExt, Stream, StreamExt}; use http::header::CONTENT_LENGTH; -use http::StatusCode; +use http::{HeaderMap, StatusCode}; use imbl_value::InternedString; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; @@ -34,51 +36,7 @@ pub async fn upload( ctx, session, |request| async move { - let headers = request.headers(); - let content_length = match headers.get(CONTENT_LENGTH).map(|a| a.to_str()) { - None => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Content-Length is required")) - .with_kind(ErrorKind::Network) - } - 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) => a, - }, - }; - - handle - .progress - .send_modify(|p| p.expected_size = Some(content_length)); - - let mut body = request.into_body().into_data_stream(); - while let Some(next) = body.next().await { - if let Err(e) = async { - handle - .write_all(&next.map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?) - .await?; - Ok(()) - } - .await - { - handle.progress.send_if_modified(|p| p.handle_error(&e)); - break; - } - } + handle.upload(request).await; Response::builder() .status(StatusCode::NO_CONTENT) @@ -364,6 +322,46 @@ pub struct UploadHandle { file: File, progress: watch::Sender, } +impl UploadHandle { + pub async fn upload(&mut self, request: Request) { + self.process_headers(request.headers()); + self.process_body(request.into_body().into_data_stream()) + .await; + } + pub async fn download(&mut self, response: reqwest::Response) { + self.process_headers(response.headers()); + self.process_body(response.bytes_stream()).await; + } + fn process_headers(&mut self, headers: &HeaderMap) { + if let Some(content_length) = headers + .get(CONTENT_LENGTH) + .and_then(|a| a.to_str().log_err()) + .and_then(|a| a.parse::().log_err()) + { + self.progress + .send_modify(|p| p.expected_size = Some(content_length)); + } + } + async fn process_body>>( + &mut self, + mut body: impl Stream> + Unpin, + ) { + while let Some(next) = body.next().await { + if let Err(e) = async { + self.write_all( + &next.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?, + ) + .await?; + Ok(()) + } + .await + { + self.progress.send_if_modified(|p| p.handle_error(&e)); + break; + } + } + } +} #[pin_project::pinned_drop] impl PinnedDrop for UploadHandle { fn drop(self: Pin<&mut Self>) { 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/io.rs b/core/startos/src/util/io.rs index bba45fa69..6d9c8a4ff 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -411,107 +411,151 @@ impl> CursorExt for Cursor { } } +#[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 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(&mut self) -> Vec { + match std::mem::take(&mut self.buffer) { + BTBuffer::Buffering { read, write } => { + self.buffer = BTBuffer::Rewound { + read: Cursor::new(read), + }; + write + } + BTBuffer::NotBuffering => Vec::new(), + BTBuffer::Rewound { read } => { + self.buffer = BTBuffer::Rewound { read }; + Vec::new() + } + } } 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 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) + } } } diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 0c9e5c5f9..fab0b127d 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -1,13 +1,16 @@ 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 color_eyre::eyre::{self, eyre}; use fd_lock_rs::FdLock; @@ -24,12 +27,17 @@ use tokio::fs::File; 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 actor; pub mod clap; +pub mod collections; pub mod cpupower; pub mod crypto; pub mod future; @@ -41,6 +49,7 @@ 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 {} @@ -555,7 +564,7 @@ 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() { @@ -648,3 +657,48 @@ pub fn new_guid() -> InternedString { &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 url.scheme() == "file" { + Ok(Self::Path(url.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 index 93131f16e..9e1beeaba 100644 --- a/core/startos/src/util/net.rs +++ b/core/startos/src/util/net.rs @@ -1,19 +1,25 @@ +use core::fmt; use std::borrow::Cow; +use std::sync::Mutex; use axum::extract::ws::{self, CloseFrame}; -use futures::Future; +use futures::{Future, Stream, StreamExt}; use crate::prelude::*; pub trait WebSocketExt { fn normal_close( self, - msg: impl Into>, - ) -> impl Future>; + 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(mut self, msg: impl Into>) -> Result<(), Error> { + async fn normal_close(mut self, msg: impl Into> + Send) -> Result<(), Error> { self.send(ws::Message::Close(Some(CloseFrame { code: 1000, reason: msg.into(), @@ -21,4 +27,41 @@ impl WebSocketExt for ws::WebSocket { .await .with_kind(ErrorKind::Network) } + 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), + } + } +} + +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 index 54664833b..80d6d9251 100644 --- a/core/startos/src/util/rpc.rs +++ b/core/startos/src/util/rpc.rs @@ -12,7 +12,7 @@ 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; +use crate::util::{Apply, PathOrUrl}; use crate::CAP_10_MiB; pub fn util() -> ParentHandler { @@ -45,21 +45,20 @@ pub async fn b3sum( } b3sum_source(file).await } - if let Ok(url) = file.parse::() { - if url.scheme() == "file" { - b3sum_file(url.path(), allow_mmap).await - } else if url.scheme() == "http" || url.scheme() == "https" { - HttpSource::new(ctx.client.clone(), url) - .await? - .apply(b3sum_source) - .await - } else { - return Err(Error::new( - eyre!("unknown scheme: {}", url.scheme()), - ErrorKind::InvalidRequest, - )); + 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, + )) + } } - } else { - b3sum_file(file, allow_mmap).await } } diff --git a/core/startos/src/util/rpc_client.rs b/core/startos/src/util/rpc_client.rs index 36fe0031a..fc93e4c64 100644 --- a/core/startos/src/util/rpc_client.rs +++ b/core/startos/src/util/rpc_client.rs @@ -138,6 +138,31 @@ impl RpcClient { 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((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)] @@ -224,4 +249,36 @@ impl UnixRpcClient { }; 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; + } + }; + res + } } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 44a69165e..88d7bfc11 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1,8 +1,10 @@ +use std::any::Any; use std::collections::VecDeque; use std::marker::PhantomData; use std::ops::Deref; use std::str::FromStr; +use base64::Engine; use clap::builder::ValueParserFactory; use clap::{ArgMatches, CommandFactory, FromArgMatches}; use color_eyre::eyre::eyre; @@ -37,7 +39,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 @@ -562,6 +568,14 @@ where #[derive(Deserialize, Serialize, TS)] 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, @@ -988,18 +1002,24 @@ impl> Serialize for Base32 { } } +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())) + 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) + BASE64 + .decode(&s) .with_kind(ErrorKind::Deserialization)? .apply(TryFrom::try_from) .map(Self) diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs new file mode 100644 index 000000000..1edd21ce1 --- /dev/null +++ b/core/startos/src/util/sync.rs @@ -0,0 +1,12 @@ +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()) + } +} diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 003a44326..18ff0e5b2 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -13,18 +13,33 @@ use crate::Error; mod v0_3_5; mod v0_3_5_1; mod v0_3_5_2; -mod v0_3_6; +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; -pub type Current = v0_3_6::Version; +pub type Current = v0_3_6_alpha_5::Version; // VERSION_BUMP #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] +#[allow(non_camel_case_types)] enum Version { LT0_3_5(LTWrapper), V0_3_5(Wrapper), V0_3_5_1(Wrapper), V0_3_5_2(Wrapper), - V0_3_6(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), Other(exver::Version), } @@ -44,7 +59,14 @@ impl Version { 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(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::Other(x) => x.clone(), } } @@ -212,6 +234,19 @@ pub async fn init( mut progress: PhaseProgressTrackerHandle, ) -> Result<(), Error> { progress.start(); + db.mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_version_mut() + .map_mutate(|v| { + Ok(if v == exver::Version::new([0, 3, 6], []) { + v0_3_6_alpha_0::Version::new().semver() + } else { + v + }) + }) + }) + .await?; // TODO: remove before releasing 0.3.6 let version = Version::from_exver_version( db.peek() .await @@ -231,7 +266,14 @@ pub async fn init( Version::V0_3_5(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, - Version::V0_3_6(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_0(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_3(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_4(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_5(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_6(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_7(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), @@ -276,6 +318,15 @@ mod tests { 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_2(Wrapper(v0_3_5_2::Version::new()))), + Just(Version::V0_3_6_alpha_0(Wrapper( + v0_3_6_alpha_0::Version::new() + ))), + Just(Version::V0_3_6_alpha_1(Wrapper( + v0_3_6_alpha_1::Version::new() + ))), + Just(Version::V0_3_6_alpha_2(Wrapper( + v0_3_6_alpha_2::Version::new() + ))), em_version().prop_map(Version::Other), ] } diff --git a/core/startos/src/version/v0_3_6.rs b/core/startos/src/version/v0_3_6_alpha_0.rs similarity index 76% rename from core/startos/src/version/v0_3_6.rs rename to core/startos/src/version/v0_3_6_alpha_0.rs index bffcfd3ef..71811981f 100644 --- a/core/startos/src/version/v0_3_6.rs +++ b/core/startos/src/version/v0_3_6_alpha_0.rs @@ -1,7 +1,8 @@ -use exver::VersionRange; +use exver::{PreReleaseSegment, VersionRange}; use super::v0_3_5::V0_3_0_COMPAT; -use super::{v0_3_5_1, VersionT}; +use super::{v0_3_5_2, VersionT}; +use crate::db::model::Database; use crate::prelude::*; use crate::{ db::model::Database, @@ -9,19 +10,22 @@ use crate::{ }; lazy_static::lazy_static! { - static ref V0_3_6: exver::Version = exver::Version::new([0, 3, 6], []); + static ref V0_3_6_alpha_0: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 0.into()] + ); } #[derive(Clone, Debug)] pub struct Version; impl VersionT for Version { - type Previous = v0_3_5_1::Version; + type Previous = v0_3_5_2::Version; fn new() -> Self { Version } fn semver(&self) -> exver::Version { - V0_3_6.clone() + V0_3_6_alpha_0.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT 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..8f40c3fde --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_1.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_0, VersionT}; +use crate::db::model::Database; +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_0::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_1.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..4b26a05dd --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_2.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_1, VersionT}; +use crate::db::model::Database; +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_1::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_2.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..3f244a2a0 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_3.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_2, VersionT}; +use crate::db::model::Database; +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_2::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_3.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..0a60764e0 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_4.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_3, VersionT}; +use crate::db::model::Database; +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_3::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_4.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..1b921d78b --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_5.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_4, VersionT}; +use crate::db::model::Database; +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_4::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_5.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..df91246ae --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_6.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_5, VersionT}; +use crate::db::model::Database; +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(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_5::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_6.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> 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..7aa63fb2e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_7.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_6, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +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, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_6::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_7.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/debian/postinst b/debian/postinst index 96e392fc8..bbf61f344 100755 --- a/debian/postinst +++ b/debian/postinst @@ -49,9 +49,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 @@ -79,6 +79,7 @@ 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 mkdir -p /etc/nginx/ssl diff --git a/image-recipe/build.sh b/image-recipe/build.sh index 5ec500ce3..5635e94f3 100755 --- a/image-recipe/build.sh +++ b/image-recipe/build.sh @@ -166,6 +166,9 @@ fi curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc > config/archives/tor.key echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg] https://deb.torproject.org/torproject.org ${IB_SUITE} main" > config/archives/tor.list +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 + # Dependencies ## Base dependencies diff --git a/sdk/.prettierignore b/sdk/.prettierignore new file mode 100644 index 000000000..19b24bbe8 --- /dev/null +++ b/sdk/.prettierignore @@ -0,0 +1 @@ +/lib/exver/exver.ts \ No newline at end of file diff --git a/sdk/Makefile b/sdk/Makefile index 4d01fa3d7..660a476c4 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -3,6 +3,8 @@ version = $(shell git tag --sort=committerdate | tail -1) .PHONY: test clean bundle fmt buildOutput check +all: bundle + test: $(TS_FILES) lib/test/output.ts npm test @@ -17,6 +19,9 @@ lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder bundle: dist | test fmt touch dist +lib/exver/exver.ts: node_modules lib/exver/exver.pegjs + npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs + dist: $(TS_FILES) package.json node_modules README.md LICENSE npx tsc npx tsc --project tsconfig-cjs.json @@ -31,7 +36,7 @@ check: npm run check fmt: node_modules - npx prettier --write "**/*.ts" + npx prettier . "**/*.ts" --write node_modules: package.json npm ci diff --git a/sdk/jest.config.js b/sdk/jest.config.js index c6aed8f3d..c38fa5062 100644 --- a/sdk/jest.config.js +++ b/sdk/jest.config.js @@ -5,4 +5,4 @@ module.exports = { testEnvironment: "node", rootDir: "./lib/", modulePathIgnorePatterns: ["./dist/"], -}; +} diff --git a/sdk/lib/Dependency.ts b/sdk/lib/Dependency.ts index 1e70629da..067ed653e 100644 --- a/sdk/lib/Dependency.ts +++ b/sdk/lib/Dependency.ts @@ -1,17 +1,17 @@ -import { Checker } from "./emverLite/mod" +import { VersionRange } from "./exver" export class Dependency { constructor( readonly data: | { type: "running" - versionSpec: Checker + versionRange: VersionRange registryUrl: string healthChecks: string[] } | { type: "exists" - versionSpec: Checker + versionRange: VersionRange registryUrl: string }, ) {} diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index fac78a4af..658597bc2 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -1,4 +1,3 @@ -import { ManifestVersion, SDKManifest } from "./manifest/ManifestTypes" import { RequiredDefault, Value } from "./config/builder/value" import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config" import { @@ -21,7 +20,6 @@ import { MaybePromise, ServiceInterfaceId, PackageId, - ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" import { DependencyConfig, Update } from "./dependencies/DependencyConfig" @@ -32,17 +30,11 @@ import { healthCheck, HealthCheckParams } from "./health/HealthCheck" import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkWebUrl, runHealthScript } from "./health/checkFns" import { List } from "./config/builder/list" -import { Migration } from "./inits/migrations/Migration" import { Install, InstallFn } from "./inits/setupInstall" import { setupActions } from "./actions/setupActions" import { setupDependencyConfig } from "./dependencies/setupDependencyConfig" import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { setupInit } from "./inits/setupInit" -import { - EnsureUniqueId, - Migrations, - setupMigrations, -} from "./inits/migrations/setupMigrations" import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall" import { setupMain } from "./mainFn" import { defaultTrigger } from "./trigger/defaultTrigger" @@ -69,16 +61,23 @@ import { } from "./util/getServiceInterface" import { getServiceInterfaces } from "./util/getServiceInterfaces" import { getStore } from "./store/getStore" -import { CommandOptions, MountOptions, Overlay } from "./util/Overlay" +import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer" import { splitCommand } from "./util/splitCommand" import { Mounts } from "./mainFn/Mounts" import { Dependency } from "./Dependency" import * as T from "./types" -import { Checker, EmVer } from "./emverLite/mod" +import { testTypeVersion, ValidateExVer } from "./exver" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" -import { checkAllDependencies } from "./dependencies/dependencies" +import { + CheckDependencies, + checkDependencies, +} from "./dependencies/dependencies" import { health } from "." +import { GetSslCertificate } from "./util/GetSslCertificate" +import { VersionGraph } from "./version" + +export const SDKVersion = testTypeVersion("0.3.6") // prettier-ignore type AnyNeverCond = @@ -88,22 +87,37 @@ type AnyNeverCond = never export type ServiceInterfaceType = "ui" | "p2p" | "api" -export type MainEffects = Effects & { _type: "main" } +export type MainEffects = Effects & { + _type: "main" + clearCallbacks: () => Promise +} export type Signals = NodeJS.Signals export const SIGTERM: Signals = "SIGTERM" export const SIGKILL: Signals = "SIGKILL" export const NO_TIMEOUT = -1 -function removeConstType() { - return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) +function removeCallbackTypes(effects: E) { + return (t: T) => { + if ("_type" in effects && effects._type === "main") { + return t as E extends MainEffects ? T : Omit + } else { + if ("const" in t) { + delete t.const + } + if ("watch" in t) { + delete t.watch + } + return t as E extends MainEffects ? T : Omit + } + } } -export class StartSdk { +export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { return new StartSdk(null as never) } - withManifest(manifest: Manifest) { + withManifest(manifest: Manifest) { return new StartSdk(manifest) } withStore>() { @@ -125,30 +139,77 @@ export class StartSdk { }]?: Dependency } + type NestedEffects = "subcontainer" | "store" + type InterfaceEffects = + | "getServiceInterface" + | "listServiceInterfaces" + | "exportServiceInterface" + | "clearServiceInterfaces" + | "bind" + | "getHostInfo" + | "getPrimaryUrl" + type MainUsedEffects = "setMainStatus" | "setHealth" + type AlreadyExposed = "getSslCertificate" | "getSystemSmtp" + + // prettier-ignore + type StartSdkEffectWrapper = { + [K in keyof Omit]: (effects: Effects, ...args: Parameters) => ReturnType + } + const startSdkEffectWrapper: StartSdkEffectWrapper = { + executeAction: (effects, ...args) => effects.executeAction(...args), + exportAction: (effects, ...args) => effects.exportAction(...args), + clearActions: (effects, ...args) => effects.clearActions(...args), + getConfigured: (effects, ...args) => effects.getConfigured(...args), + setConfigured: (effects, ...args) => effects.setConfigured(...args), + 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), + } + return { - checkAllDependencies, + ...startSdkEffectWrapper, + + checkDependencies: checkDependencies as < + DependencyId extends keyof Manifest["dependencies"] & + PackageId = keyof Manifest["dependencies"] & PackageId, + >( + effects: Effects, + packageIds?: DependencyId[], + ) => Promise>, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => - removeConstType()( + removeCallbackTypes(effects)( getServiceInterface(effects, { id, - packageId: null, }), ), get: ( effects: E, opts: { id: ServiceInterfaceId; packageId: PackageId }, - ) => removeConstType()(getServiceInterface(effects, opts)), + ) => + removeCallbackTypes(effects)(getServiceInterface(effects, opts)), getAllOwn: (effects: E) => - removeConstType()( - getServiceInterfaces(effects, { - packageId: null, - }), - ), + removeCallbackTypes(effects)(getServiceInterfaces(effects, {})), getAll: ( effects: E, opts: { packageId: PackageId }, - ) => removeConstType()(getServiceInterfaces(effects, opts)), + ) => + removeCallbackTypes(effects)(getServiceInterfaces(effects, opts)), }, store: { @@ -157,7 +218,7 @@ export class StartSdk { packageId: string, path: PathBuilder, ) => - removeConstType()( + removeCallbackTypes(effects)( getStore(effects, path, { packageId, }), @@ -165,7 +226,10 @@ export class StartSdk { getOwn: ( effects: E, path: PathBuilder, - ) => removeConstType()(getStore(effects, path)), + ) => + removeCallbackTypes(effects)( + getStore(effects, path), + ), setOwn: >( effects: E, path: Path, @@ -191,7 +255,7 @@ export class StartSdk { id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, @@ -231,7 +295,6 @@ export class StartSdk { id: string description: string hasPrimary: boolean - disabled: boolean type: ServiceInterfaceType username: null | string path: string @@ -241,7 +304,16 @@ export class StartSdk { }, ) => new ServiceInterfaceBuilder({ ...options, effects }), getSystemSmtp: (effects: E) => - removeConstType()(new GetSystemSmtp(effects)), + removeCallbackTypes(effects)(new GetSystemSmtp(effects)), + + getSslCerificate: ( + effects: E, + hostnames: string[], + algorithm?: T.Algorithm, + ) => + removeCallbackTypes(effects)( + new GetSslCertificate(effects, hostnames, algorithm), + ), createDynamicAction: < ConfigType extends @@ -268,8 +340,8 @@ export class StartSdk { ) }, HealthCheck: { - of(o: HealthCheckParams) { - return healthCheck(o) + of(o: HealthCheckParams) { + return healthCheck(o) }, }, Dependency: { @@ -286,7 +358,7 @@ export class StartSdk { setupActions: (...createdActions: CreatedAction[]) => setupActions(...createdActions), setupBackups: (...args: SetupBackupsParams) => - setupBackups(...args), + setupBackups(this.manifest, ...args), setupConfig: < ConfigType extends Config | Config, Type extends Record = ExtractConfigType, @@ -335,7 +407,7 @@ export class StartSdk { ([ id, { - data: { versionSpec, ...x }, + data: { versionRange, ...x }, }, ]) => ({ id, @@ -348,14 +420,14 @@ export class StartSdk { : { kind: "exists", }), - versionSpec: versionSpec.range, + versionRange: versionRange.toString(), }), ), }) } }, setupInit: ( - migrations: Migrations, + versions: VersionGraph, install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, @@ -366,7 +438,7 @@ export class StartSdk { exposedStore: ExposedStorePaths, ) => setupInit( - migrations, + versions, install, uninstall, setInterfaces, @@ -387,15 +459,6 @@ export class StartSdk { started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, ) => setupMain(fn), - setupMigrations: < - Migrations extends Array>, - >( - ...migrations: EnsureUniqueId - ) => - setupMigrations( - this.manifest, - ...migrations, - ), setupProperties: ( fn: (options: { effects: Effects }) => Promise, @@ -432,9 +495,6 @@ export class StartSdk { spec: Spec, ) => Config.of(spec), }, - Checker: { - parse: Checker.parse, - }, Daemons: { of(config: { effects: Effects @@ -474,10 +534,6 @@ export class StartSdk { >(dependencyConfig, update) }, }, - EmVer: { - from: EmVer.from, - parse: EmVer.parse, - }, List: { text: List.text, obj: >( @@ -523,13 +579,6 @@ export class StartSdk { >, ) => List.dynamicText(getA), }, - Migration: { - of: (options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise - }) => Migration.of(options), - }, StorePath: pathBuilder(), Value: { toggle: Value.toggle, @@ -720,7 +769,7 @@ export class StartSdk { } } -export async function runCommand( +export async function runCommand( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], @@ -729,15 +778,12 @@ export async function runCommand( }, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { const commands = splitCommand(command) - const overlay = await Overlay.of(effects, image) - try { - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } - return await overlay.exec(commands) - } finally { - await overlay.destroy() - } + return SubContainer.with( + effects, + image, + options.mounts || [], + (subcontainer) => subcontainer.exec(commands), + ) } function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn { return Object.fromEntries( diff --git a/sdk/lib/actions/createAction.ts b/sdk/lib/actions/createAction.ts index 2fe4dfa74..4fa858d56 100644 --- a/sdk/lib/actions/createAction.ts +++ b/sdk/lib/actions/createAction.ts @@ -1,12 +1,13 @@ +import * as T from "../types" import { Config, ExtractConfigType } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" -export type MaybeFn = +export type MaybeFn = | Value | ((options: { effects: Effects }) => Promise | Value) export class CreatedAction< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record @@ -30,7 +31,7 @@ export class CreatedAction< ) {} static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record diff --git a/sdk/lib/actions/setupActions.ts b/sdk/lib/actions/setupActions.ts index 9dd9937b4..07b4e2606 100644 --- a/sdk/lib/actions/setupActions.ts +++ b/sdk/lib/actions/setupActions.ts @@ -1,8 +1,8 @@ -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" import { Effects, ExpectedExports } from "../types" import { CreatedAction } from "./createAction" -export function setupActions( +export function setupActions( ...createdActions: CreatedAction[] ) { const myActions = async (options: { effects: Effects }) => { diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 20099c86d..031ac4e4c 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -1,6 +1,9 @@ -import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types" +import * as child_process from "child_process" +import { promises as fsPromises } from "fs" +import { asError } from "../util" + export type BACKUP = "BACKUP" export const DEFAULT_OPTIONS: T.BackupOptions = { delete: true, @@ -37,14 +40,14 @@ export type BackupSet = { * ).build()q * ``` */ -export class Backups { +export class Backups { static BACKUP: BACKUP = "BACKUP" private constructor( private options = DEFAULT_OPTIONS, private backupSet = [] as BackupSet[], ) {} - static volumes( + static volumes( ...volumeNames: Array ): Backups { return new Backups().addSets( @@ -56,12 +59,12 @@ export class Backups { })), ) } - static addSets( + static addSets( ...options: BackupSet[] ) { return new Backups().addSets(...options) } - static with_options( + static with_options( options?: Partial, ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }) @@ -91,58 +94,22 @@ export class Backups { ) return this } - build() { + build(pathMaker: T.PathMaker) { const createBackup: T.ExpectedExports.createBackup = async ({ effects, }) => { - // const previousItems = ( - // await effects - // .readDir({ - // volumeId: Backups.BACKUP, - // path: ".", - // }) - // .catch(() => []) - // ).map((x) => `${x}`) - // const backupPaths = this.backupSet - // .filter((x) => x.dstVolume === Backups.BACKUP) - // .map((x) => x.dstPath) - // .map((x) => x.replace(/\.\/([^]*)\//, "$1")) - // const filteredItems = previousItems.filter( - // (x) => backupPaths.indexOf(x) === -1, - // ) - // for (const itemToRemove of filteredItems) { - // effects.console.error(`Trying to remove ${itemToRemove}`) - // await effects - // .removeDir({ - // volumeId: Backups.BACKUP, - // path: itemToRemove, - // }) - // .catch(() => - // effects.removeFile({ - // volumeId: Backups.BACKUP, - // path: itemToRemove, - // }), - // ) - // .catch(() => { - // console.warn(`Failed to remove ${itemToRemove} from backup volume`) - // }) - // } for (const item of this.backupSet) { - // if (notEmptyPath(item.dstPath)) { - // await effects.createDir({ - // volumeId: item.dstVolume, - // path: item.dstPath, - // }) - // } - // await effects - // .runRsync({ - // ...item, - // options: { - // ...this.options, - // ...item.options, - // }, - // }) - // .wait() + const rsyncResults = await runRsync( + { + dstPath: item.dstPath, + dstVolume: item.dstVolume, + options: { ...this.options, ...item.options }, + srcPath: item.srcPath, + srcVolume: item.srcVolume, + }, + pathMaker, + ) + await rsyncResults.wait() } return } @@ -150,26 +117,17 @@ export class Backups { effects, }) => { for (const item of this.backupSet) { - // if (notEmptyPath(item.srcPath)) { - // await new Promise((resolve, reject) => fs.mkdir(items.src)).createDir( - // { - // volumeId: item.srcVolume, - // path: item.srcPath, - // }, - // ) - // } - // await effects - // .runRsync({ - // options: { - // ...this.options, - // ...item.options, - // }, - // srcVolume: item.dstVolume, - // dstVolume: item.srcVolume, - // srcPath: item.dstPath, - // dstPath: item.srcPath, - // }) - // .wait() + const rsyncResults = await runRsync( + { + dstPath: item.dstPath, + dstVolume: item.dstVolume, + options: { ...this.options, ...item.options }, + srcPath: item.srcPath, + srcVolume: item.srcVolume, + }, + pathMaker, + ) + await rsyncResults.wait() } return } @@ -179,3 +137,73 @@ export class Backups { function notEmptyPath(file: string) { return ["", ".", "./"].indexOf(file) === -1 } +async function runRsync( + rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: T.BackupOptions + }, + pathMaker: T.PathMaker, +): Promise<{ + 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(pathMaker({ volume: srcVolume, path: srcPath })) + args.push(pathMaker({ volume: dstVolume, path: 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/lib/backup/setupBackups.ts b/sdk/lib/backup/setupBackups.ts index af2d08410..c12f1d2ed 100644 --- a/sdk/lib/backup/setupBackups.ts +++ b/sdk/lib/backup/setupBackups.ts @@ -1,13 +1,14 @@ import { Backups } from "./Backups" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" + +import * as T from "../types" import { _ } from "../util" -export type SetupBackupsParams = Array< +export type SetupBackupsParams = Array< M["volumes"][number] | Backups > -export function setupBackups( +export function setupBackups( + manifest: M, ...args: _> ) { const backups = Array>() @@ -21,22 +22,23 @@ export function setupBackups( } backups.push(Backups.volumes(...volumes)) const answer: { - createBackup: ExpectedExports.createBackup - restoreBackup: ExpectedExports.restoreBackup + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup } = { get createBackup() { return (async (options) => { for (const backup of backups) { - await backup.build().createBackup(options) + await backup.build(options.pathMaker).createBackup(options) } - }) as ExpectedExports.createBackup + }) as T.ExpectedExports.createBackup }, get restoreBackup() { return (async (options) => { for (const backup of backups) { - await backup.build().restoreBackup(options) + await backup.build(options.pathMaker).restoreBackup(options) } - }) as ExpectedExports.restoreBackup + await options.effects.setDataVersion({ version: manifest.version }) + }) as T.ExpectedExports.restoreBackup }, } return answer diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index 2ab091e18..d9865f25c 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -1,22 +1,21 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Dependencies } from "../types" +import * as T from "../types" -export type ConfigDependencies = { - exists(id: keyof T["dependencies"]): Dependencies[number] +export type ConfigDependencies = { + exists(id: keyof T["dependencies"]): T.Dependencies[number] running( id: keyof T["dependencies"], healthChecks: string[], - ): Dependencies[number] + ): T.Dependencies[number] } export const configDependenciesSet = < - T extends SDKManifest, + T extends T.Manifest, >(): ConfigDependencies => ({ exists(id: keyof T["dependencies"]) { return { id, kind: "exists", - } as Dependencies[number] + } as T.Dependencies[number] }, running(id: keyof T["dependencies"], healthChecks: string[]) { @@ -24,6 +23,6 @@ export const configDependenciesSet = < id, kind: "running", healthChecks, - } as Dependencies[number] + } as T.Dependencies[number] }, }) diff --git a/sdk/lib/config/configTypes.ts b/sdk/lib/config/configTypes.ts index 14d857433..0179e531e 100644 --- a/sdk/lib/config/configTypes.ts +++ b/sdk/lib/config/configTypes.ts @@ -240,7 +240,6 @@ export type ListValueSpecText = { inputmode: "text" | "email" | "tel" | "url" placeholder: string | null } - export type ListValueSpecObject = { type: "object" /** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */ diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index ba82dbad6..f354c81ed 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -1,5 +1,5 @@ -import { Effects, ExpectedExports } from "../types" -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" + import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" import nullIfEmpty from "../util/nullIfEmpty" @@ -16,7 +16,7 @@ export type Save< | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects input: ExtractConfigType & Record }) => Promise<{ dependenciesReceipt: DependenciesReceipt @@ -24,14 +24,14 @@ export type Save< restart: boolean }> export type Read< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, A extends | Record | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -46,7 +46,7 @@ export function setupConfig< | Record | Config | Config, - Manifest extends SDKManifest, + Manifest extends T.Manifest, Type extends Record = ExtractConfigType, >( spec: Config | Config, @@ -57,7 +57,9 @@ export function setupConfig< return { setConfig: (async ({ effects, input }) => { if (!validator.test(input)) { - await console.error(String(validator.errorMessage(input))) + await console.error( + new Error(validator.errorMessage(input)?.toString()), + ) return { error: "Set config type error for config" } } await effects.clearBindings() @@ -69,7 +71,7 @@ export function setupConfig< if (restart) { await effects.restart() } - }) as ExpectedExports.setConfig, + }) as T.ExpectedExports.setConfig, getConfig: (async ({ effects }) => { const configValue = nullIfEmpty((await read({ effects })) || null) return { @@ -78,7 +80,7 @@ export function setupConfig< }), config: configValue, } - }) as ExpectedExports.getConfig, + }) as T.ExpectedExports.getConfig, } } diff --git a/sdk/lib/dependencies/DependencyConfig.ts b/sdk/lib/dependencies/DependencyConfig.ts index d7ce435ad..b48bf56d3 100644 --- a/sdk/lib/dependencies/DependencyConfig.ts +++ b/sdk/lib/dependencies/DependencyConfig.ts @@ -1,11 +1,6 @@ -import { - DependencyConfig as DependencyConfigType, - DeepPartial, - Effects, -} from "../types" +import * as T from "../types" import { deepEqual } from "../util/deepEqual" import { deepMerge } from "../util/deepMerge" -import { SDKManifest } from "../manifest/ManifestTypes" export type Update = (options: { remoteConfig: RemoteConfig @@ -13,7 +8,7 @@ export type Update = (options: { }) => Promise export class DependencyConfig< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Input extends Record, RemoteConfig extends Record, @@ -26,16 +21,16 @@ export class DependencyConfig< } constructor( readonly dependencyConfig: (options: { - effects: Effects + effects: T.Effects localConfig: Input - }) => Promise>, + }) => Promise>, readonly update: Update< - void | DeepPartial, + void | T.DeepPartial, RemoteConfig > = DependencyConfig.defaultUpdate as any, ) {} - async query(options: { effects: Effects; localConfig: unknown }) { + async query(options: { effects: T.Effects; localConfig: unknown }) { return this.dependencyConfig({ localConfig: options.localConfig as Input, effects: options.effects, diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts index c074d2ad7..287f63b06 100644 --- a/sdk/lib/dependencies/dependencies.ts +++ b/sdk/lib/dependencies/dependencies.ts @@ -1,115 +1,206 @@ +import { ExtendedVersion, VersionRange } from "../exver" import { Effects, PackageId, DependencyRequirement, SetHealth, - CheckDependencyResult, + CheckDependenciesResult, + HealthCheckId, } from "../types" -export type CheckAllDependencies = { - notRunning: () => Promise +export type CheckDependencies = { + installedSatisfied: (packageId: DependencyId) => boolean + installedVersionSatisfied: (packageId: DependencyId) => boolean + runningSatisfied: (packageId: DependencyId) => boolean + configSatisfied: (packageId: DependencyId) => boolean + healthCheckSatisfied: ( + packageId: DependencyId, + healthCheckId: HealthCheckId, + ) => boolean + satisfied: () => boolean - notInstalled: () => Promise - - healthErrors: () => Promise<{ [id: string]: SetHealth[] }> - throwIfNotRunning: () => Promise - throwIfNotValid: () => Promise - throwIfNotInstalled: () => Promise - throwIfError: () => Promise - isValid: () => Promise + throwIfInstalledNotSatisfied: (packageId: DependencyId) => void + throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void + throwIfRunningNotSatisfied: (packageId: DependencyId) => void + throwIfConfigNotSatisfied: (packageId: DependencyId) => void + throwIfHealthNotSatisfied: ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => void + throwIfNotSatisfied: (packageId?: DependencyId) => void } -export function checkAllDependencies(effects: Effects): CheckAllDependencies { - const dependenciesPromise = effects.getDependencies() - const resultsPromise = dependenciesPromise.then((dependencies) => +export async function checkDependencies< + DependencyId extends PackageId = PackageId, +>( + effects: Effects, + packageIds?: DependencyId[], +): Promise> { + let [dependencies, results] = await Promise.all([ + effects.getDependencies(), effects.checkDependencies({ - packageIds: dependencies.map((dep) => dep.id), + packageIds, }), - ) + ]) + if (packageIds) { + dependencies = dependencies.filter((d) => + (packageIds as PackageId[]).includes(d.id), + ) + } - const dependenciesByIdPromise = dependenciesPromise.then((d) => - d.reduce( - (acc, dep) => { - acc[dep.id] = dep - return acc - }, - {} as { [id: PackageId]: DependencyRequirement }, - ), - ) + 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 healthErrors = async () => { - const results = await resultsPromise - const dependenciesById = await dependenciesByIdPromise - const answer: { [id: PackageId]: SetHealth[] } = {} - for (const result of results) { - const dependency = dependenciesById[result.packageId] - if (!dependency) continue - if (dependency.kind !== "running") continue + 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 configSatisfied = (packageId: DependencyId) => + find(packageId).result.configSatisfied + 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) && + configSatisfied(packageId) && + healthCheckSatisfied(packageId) + const satisfied = (packageId?: DependencyId) => + packageId + ? pkgSatisfied(packageId) + : dependencies.every((d) => pkgSatisfied(d.id as DependencyId)) - const healthChecks = result.healthChecks - .filter((x) => dependency.healthChecks.includes(x.id)) - .filter((x) => !!x.message) - if (healthChecks.length === 0) continue - answer[result.packageId] = healthChecks + const throwIfInstalledNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.installedVersion) { + throw new Error(`${dep.result.title || packageId} is not installed`) } - return answer } - const notInstalled = () => - resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) - const notRunning = async () => { - const results = await resultsPromise - const dependenciesById = await dependenciesByIdPromise - return results.filter((x) => { - const dependency = dependenciesById[x.packageId] - if (!dependency) return false - if (dependency.kind !== "running") return false - return !x.isRunning - }) + 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}`, + ) + } } - const entries = (x: { [k: string]: B }) => Object.entries(x) - const first = (x: A[]): A | undefined => x[0] - const sinkVoid = (x: A) => void 0 - const throwIfError = () => - healthErrors() - .then(entries) - .then(first) - .then((x) => { - if (!x) return - const [id, healthChecks] = x - if (healthChecks.length > 0) - throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` - }) - const throwIfNotRunning = () => - notRunning().then((results) => { - if (results[0]) - throw new Error(`Package ${results[0].packageId} is not running`) - }) - - const throwIfNotInstalled = () => - notInstalled().then((results) => { - if (results[0]) - throw new Error(`Package ${results[0].packageId} is not installed`) - }) - const throwIfNotValid = async () => - Promise.all([ - throwIfNotRunning(), - throwIfNotInstalled(), - throwIfError(), - ]).then(sinkVoid) - - const isValid = () => - throwIfNotValid().then( - () => true, - () => false, - ) + 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`) + } + } + const throwIfConfigNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.configSatisfied) { + throw new Error( + `${dep.result.title || packageId}'s configuration does not satisfy requirements`, + ) + } + } + 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("; "), + ) + } + } + const throwIfPkgNotSatisfied = (packageId: DependencyId) => { + throwIfInstalledNotSatisfied(packageId) + throwIfInstalledVersionNotSatisfied(packageId) + throwIfRunningNotSatisfied(packageId) + throwIfConfigNotSatisfied(packageId) + throwIfHealthNotSatisfied(packageId) + } + 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 { - notRunning, - notInstalled, - healthErrors, - throwIfNotRunning, - throwIfNotValid, - throwIfNotInstalled, - throwIfError, - isValid, + installedSatisfied, + installedVersionSatisfied, + runningSatisfied, + configSatisfied, + healthCheckSatisfied, + satisfied, + throwIfInstalledNotSatisfied, + throwIfInstalledVersionNotSatisfied, + throwIfRunningNotSatisfied, + throwIfConfigNotSatisfied, + throwIfHealthNotSatisfied, + throwIfNotSatisfied, } } diff --git a/sdk/lib/dependencies/setupDependencyConfig.ts b/sdk/lib/dependencies/setupDependencyConfig.ts index c67c46a44..2fde4bce5 100644 --- a/sdk/lib/dependencies/setupDependencyConfig.ts +++ b/sdk/lib/dependencies/setupDependencyConfig.ts @@ -1,12 +1,12 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" + +import * as T from "../types" import { DependencyConfig } from "./DependencyConfig" export function setupDependencyConfig< Store, Input extends Record, - Manifest extends SDKManifest, + Manifest extends T.Manifest, >( _config: Config | Config, autoConfigs: { @@ -17,6 +17,6 @@ export function setupDependencyConfig< any > | null }, -): ExpectedExports.dependencyConfig { +): T.ExpectedExports.dependencyConfig { return autoConfigs } diff --git a/sdk/lib/emverLite/mod.ts b/sdk/lib/emverLite/mod.ts deleted file mode 100644 index 52fb4e347..000000000 --- a/sdk/lib/emverLite/mod.ts +++ /dev/null @@ -1,323 +0,0 @@ -import * as matches from "ts-matches" - -const starSub = /((\d+\.)*\d+)\.\*/ -// prettier-ignore -export type ValidEmVer = string; -// prettier-ignore -export type ValidEmVerRange = string; - -function incrementLastNumber(list: number[]) { - const newList = [...list] - newList[newList.length - 1]++ - return newList -} -/** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ -export function rangeOf(range: string | Checker): Checker { - return Checker.parse(range) -} - -/** - * Used to create a checker that will `and` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeAnd(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).and(...rest) -} - -/** - * Used to create a checker that will `or` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeOr(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).or(...rest) -} - -/** - * This will negate the checker, so given a checker that checks for >= 1.0.0, it will check for < 1.0.0 - * @param range - * @returns - */ -export function notRange(range: string | Checker): Checker { - return rangeOf(range).not() -} - -/** - * EmVer is a set of versioning of any pattern like 1 or 1.2 or 1.2.3 or 1.2.3.4 or .. - */ -export class EmVer { - /** - * Convert the range, should be 1.2.* or * into a emver - * Or an already made emver - * IsUnsafe - */ - static from(range: string | EmVer): EmVer { - if (range instanceof EmVer) { - return range - } - return EmVer.parse(range) - } - /** - * Convert the range, should be 1.2.* or * into a emver - * IsUnsafe - */ - static parse(rangeExtra: string): EmVer { - const [range, extra] = rangeExtra.split("-") - const values = range.split(".").map((x) => parseInt(x)) - for (const value of values) { - if (isNaN(value)) { - throw new Error(`Couldn't parse range: ${range}`) - } - } - return new EmVer(values, extra) - } - private constructor( - public readonly values: number[], - readonly extra: string | null, - ) {} - - /** - * Used when we need a new emver that has the last number incremented, used in the 1.* like things - */ - public withLastIncremented() { - return new EmVer(incrementLastNumber(this.values), null) - } - - public greaterThan(other: EmVer): boolean { - for (const i in this.values) { - if (other.values[i] == null) { - return true - } - if (this.values[i] > other.values[i]) { - return true - } - - if (this.values[i] < other.values[i]) { - return false - } - } - return false - } - - public equals(other: EmVer): boolean { - if (other.values.length !== this.values.length) { - return false - } - for (const i in this.values) { - if (this.values[i] !== other.values[i]) { - return false - } - } - return true - } - public greaterThanOrEqual(other: EmVer): boolean { - return this.greaterThan(other) || this.equals(other) - } - public lessThanOrEqual(other: EmVer): boolean { - return !this.greaterThan(other) - } - public lessThan(other: EmVer): boolean { - return !this.greaterThanOrEqual(other) - } - /** - * Return a enum string that describes (used for switching/iffs) - * to know comparison - * @param other - * @returns - */ - public compare(other: EmVer) { - if (this.equals(other)) { - return "equal" as const - } else if (this.greaterThan(other)) { - return "greater" as const - } else { - return "less" as const - } - } - /** - * Used when sorting emver's in a list using the sort method - * @param other - * @returns - */ - public compareForSort(other: EmVer) { - return matches - .matches(this.compare(other)) - .when("equal", () => 0 as const) - .when("greater", () => 1 as const) - .when("less", () => -1 as const) - .unwrap() - } - - toString() { - return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}` as ValidEmVer - } -} - -/** - * A checker is a function that takes a version and returns true if the version matches the checker. - * Used when we are doing range checking, like saying ">=1.0.0".check("1.2.3") will be true - */ -export class Checker { - /** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ - static parse(range: string | Checker): Checker { - if (range instanceof Checker) { - return range - } - range = range.trim() - if (range.indexOf("||") !== -1) { - return rangeOr(...range.split("||").map((x) => Checker.parse(x))) - } - if (range.indexOf("&&") !== -1) { - return rangeAnd(...range.split("&&").map((x) => Checker.parse(x))) - } - if (range === "*") { - return new Checker((version) => { - EmVer.from(version) - return true - }, range) - } - if (range.startsWith("!!")) return Checker.parse(range.substring(2)) - if (range.startsWith("!")) { - const tempValue = Checker.parse(range.substring(1)) - return new Checker((x) => !tempValue.check(x), range) - } - const starSubMatches = starSub.exec(range) - if (starSubMatches != null) { - const emVarLower = EmVer.parse(starSubMatches[1]) - const emVarUpper = emVarLower.withLastIncremented() - - return new Checker((version) => { - const v = EmVer.from(version) - return ( - (v.greaterThan(emVarLower) || v.equals(emVarLower)) && - !v.greaterThan(emVarUpper) && - !v.equals(emVarUpper) - ) - }, range) - } - - switch (range.substring(0, 2)) { - case ">=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThanOrEqual(emVar) - }, range) - } - case "<=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThanOrEqual(emVar) - }, range) - } - } - - switch (range.substring(0, 1)) { - case ">": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThan(emVar) - }, range) - } - case "<": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThan(emVar) - }, range) - } - case "=": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.equals(emVar) - }, `=${emVar.toString()}`) - } - } - throw new Error("Couldn't parse range: " + range) - } - constructor( - /** - * Check is the function that will be given a emver or unparsed emver and should give if it follows - * a pattern - */ - public readonly check: (value: ValidEmVer | EmVer) => boolean, - private readonly _range: string, - ) {} - - get range() { - return this._range as ValidEmVerRange - } - - /** - * Used when we want the `and` condition with another checker - */ - public and(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (!this.check(value)) { - return false - } - for (const other of othersCheck) { - if (!other.check(value)) { - return false - } - } - return true - }, - othersCheck.map((x) => x._range).join(" && "), - ) - } - - /** - * Used when we want the `or` condition with another checker - */ - public or(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (this.check(value)) { - return true - } - for (const other of othersCheck) { - if (other.check(value)) { - return true - } - } - return false - }, - othersCheck.map((x) => x._range).join(" || "), - ) - } - - /** - * A useful example is making sure we don't match an exact version, like !=1.2.3 - * @returns - */ - public not(): Checker { - let newRange = `!${this._range}` - return Checker.parse(newRange) - } -} diff --git a/sdk/lib/exver/exver.pegjs b/sdk/lib/exver/exver.pegjs new file mode 100644 index 000000000..3045b9224 --- /dev/null +++ b/sdk/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/lib/exver/exver.ts b/sdk/lib/exver/exver.ts new file mode 100644 index 000000000..be9ea3e0e --- /dev/null +++ b/sdk/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/lib/exver/index.ts b/sdk/lib/exver/index.ts new file mode 100644 index 000000000..331271c1a --- /dev/null +++ b/sdk/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/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 1ed8652bf..e007c4ea2 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -1,75 +1,61 @@ -import { InterfaceReceipt } from "../interfaces/interfaceReceipt" -import { Daemon, Effects, SDKManifest } from "../types" -import { CheckResult } from "./checkFns/CheckResult" +import { Effects } from "../types" +import { HealthCheckResult } from "./checkFns/HealthCheckResult" import { HealthReceipt } from "./HealthReceipt" import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" -import { Overlay } from "../util/Overlay" +import { SubContainer } from "../util/SubContainer" import { object, unknown } from "ts-matches" -import { T } from ".." +import * as T from "../types" +import { asError } from "../util/asError" -export type HealthCheckParams = { +export type HealthCheckParams = { effects: Effects name: string - image: { - id: keyof Manifest["images"] & T.ImageId - sharedRun?: boolean - } trigger?: Trigger - fn(overlay: Overlay): Promise | CheckResult + fn(): Promise | HealthCheckResult onFirstSuccess?: () => unknown | Promise } -export function healthCheck( - o: HealthCheckParams, -) { +export function healthCheck(o: HealthCheckParams) { new Promise(async () => { - const overlay = await Overlay.of(o.effects, o.image) - try { - let currentValue: TriggerInput = { - hadSuccess: false, + 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" } - 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 { status, message } = await o.fn(overlay) - await o.effects.setHealth({ - name: o.name, - id: o.name, - result: status, - message: message || "", - }) - currentValue.hadSuccess = true - currentValue.lastResult = "success" - await triggerFirstSuccess().catch((err) => { - console.error(err) - }) - } catch (e) { - await o.effects.setHealth({ - name: o.name, - id: o.name, - result: "failure", - message: asMessage(e) || "", - }) - currentValue.lastResult = "failure" - } - } - } finally { - await overlay.destroy() } }) return {} as HealthReceipt diff --git a/sdk/lib/health/checkFns/CheckResult.ts b/sdk/lib/health/checkFns/CheckResult.ts deleted file mode 100644 index 8b46ee5c4..000000000 --- a/sdk/lib/health/checkFns/CheckResult.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HealthStatus } from "../../types" - -export type CheckResult = { - status: HealthStatus - message: string | null -} diff --git a/sdk/lib/health/checkFns/HealthCheckResult.ts b/sdk/lib/health/checkFns/HealthCheckResult.ts new file mode 100644 index 000000000..ba2468488 --- /dev/null +++ b/sdk/lib/health/checkFns/HealthCheckResult.ts @@ -0,0 +1,3 @@ +import { T } from "../.." + +export type HealthCheckResult = Omit diff --git a/sdk/lib/health/checkFns/checkPortListening.ts b/sdk/lib/health/checkFns/checkPortListening.ts index 4cc0738da..94d0becc0 100644 --- a/sdk/lib/health/checkFns/checkPortListening.ts +++ b/sdk/lib/health/checkFns/checkPortListening.ts @@ -1,6 +1,6 @@ import { Effects } from "../../types" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" -import { CheckResult } from "./CheckResult" +import { HealthCheckResult } from "./HealthCheckResult" import { promisify } from "node:util" import * as CP from "node:child_process" @@ -32,8 +32,8 @@ export async function checkPortListening( timeoutMessage?: string timeout?: number }, -): Promise { - return Promise.race([ +): Promise { + return Promise.race([ Promise.resolve().then(async () => { const hasAddress = containsAddress( @@ -45,10 +45,10 @@ export async function checkPortListening( port, ) if (hasAddress) { - return { status: "success", message: options.successMessage } + return { result: "success", message: options.successMessage } } return { - status: "failure", + result: "failure", message: options.errorMessage, } }), @@ -56,7 +56,7 @@ export async function checkPortListening( setTimeout( () => resolve({ - status: "failure", + result: "failure", message: options.timeoutMessage || `Timeout trying to check port ${port}`, }), diff --git a/sdk/lib/health/checkFns/checkWebUrl.ts b/sdk/lib/health/checkFns/checkWebUrl.ts index 8f61ae2ef..042115211 100644 --- a/sdk/lib/health/checkFns/checkWebUrl.ts +++ b/sdk/lib/health/checkFns/checkWebUrl.ts @@ -1,5 +1,6 @@ import { Effects } from "../../types" -import { CheckResult } from "./CheckResult" +import { asError } from "../../util/asError" +import { HealthCheckResult } from "./HealthCheckResult" import { timeoutPromise } from "./index" import "isomorphic-fetch" @@ -17,19 +18,19 @@ export const checkWebUrl = async ( successMessage = `Reached ${url}`, errorMessage = `Error while fetching URL: ${url}`, } = {}, -): Promise => { +): Promise => { return Promise.race([fetch(url), timeoutPromise(timeout)]) .then( (x) => ({ - status: "success", + result: "success", message: successMessage, }) as const, ) .catch((e) => { console.warn(`Error while fetching URL: ${url}`) console.error(JSON.stringify(e)) - console.error(e.toString()) - return { status: "failure" as const, message: errorMessage } + console.error(asError(e)) + return { result: "failure" as const, message: errorMessage } }) } diff --git a/sdk/lib/health/checkFns/index.ts b/sdk/lib/health/checkFns/index.ts index d33d5ad0d..2de37e38c 100644 --- a/sdk/lib/health/checkFns/index.ts +++ b/sdk/lib/health/checkFns/index.ts @@ -1,6 +1,6 @@ import { runHealthScript } from "./runHealthScript" export { checkPortListening } from "./checkPortListening" -export { CheckResult } from "./CheckResult" +export { HealthCheckResult } from "./HealthCheckResult" export { checkWebUrl } from "./checkWebUrl" export function timeoutPromise(ms: number, { message = "Timed out" } = {}) { diff --git a/sdk/lib/health/checkFns/runHealthScript.ts b/sdk/lib/health/checkFns/runHealthScript.ts index f0f41ee91..4bac211a9 100644 --- a/sdk/lib/health/checkFns/runHealthScript.ts +++ b/sdk/lib/health/checkFns/runHealthScript.ts @@ -1,7 +1,7 @@ import { Effects } from "../../types" -import { Overlay } from "../../util/Overlay" +import { SubContainer } from "../../util/SubContainer" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" -import { CheckResult } from "./CheckResult" +import { HealthCheckResult } from "./HealthCheckResult" import { timeoutPromise } from "./index" /** @@ -12,27 +12,26 @@ import { timeoutPromise } from "./index" * @returns */ export const runHealthScript = async ( - effects: Effects, runCommand: string[], - overlay: Overlay, + subcontainer: SubContainer, { timeout = 30000, errorMessage = `Error while running command: ${runCommand}`, message = (res: string) => `Have ran script ${runCommand} and the result: ${res}`, } = {}, -): Promise => { +): Promise => { const res = await Promise.race([ - overlay.exec(runCommand), + subcontainer.exec(runCommand), timeoutPromise(timeout), ]).catch((e) => { console.warn(errorMessage) console.warn(JSON.stringify(e)) console.warn(e.toString()) - throw { status: "failure", message: errorMessage } as CheckResult + throw { result: "failure", message: errorMessage } as HealthCheckResult }) return { - status: "success", + result: "success", message: message(res.stdout.toString()), - } as CheckResult + } as HealthCheckResult } diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index c7ab45e60..f7d645133 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -1,12 +1,10 @@ -export { EmVer } from "./emverLite/mod" -export { setupManifest } from "./manifest/setupManifest" -export { setupExposeStore } from "./store/setupExposeStore" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" + export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" export * as dependencyConfig from "./dependencies" -export * as manifest from "./manifest" export * as types from "./types" export * as T from "./types" export * as yaml from "yaml" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index c99798d72..a4caf9c01 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,12 +1,12 @@ export { Daemons } from "./mainFn/Daemons" -export { EmVer } from "./emverLite/mod" -export { Overlay } from "./util/Overlay" +export { SubContainer } from "./util/SubContainer" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" export { setupExposeStore } from "./store/setupExposeStore" export { pathBuilder } from "./store/PathBuilder" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" export * as actions from "./actions" export * as backup from "./backup" @@ -29,3 +29,4 @@ export * as utils from "./util" export * as matches from "ts-matches" export * as YAML from "yaml" export * as TOML from "@iarna/toml" +export * from "./version" diff --git a/sdk/lib/inits/migrations/Migration.ts b/sdk/lib/inits/migrations/Migration.ts deleted file mode 100644 index 119271aea..000000000 --- a/sdk/lib/inits/migrations/Migration.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" -import { Effects } from "../../types" - -export class Migration< - Manifest extends SDKManifest, - Store, - Version extends ManifestVersion, -> { - constructor( - readonly options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise - }, - ) {} - static of< - Manifest extends SDKManifest, - Store, - Version extends ManifestVersion, - >(options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise - }) { - return new Migration(options) - } - - async up(opts: { effects: Effects }) { - this.up(opts) - } - - async down(opts: { effects: Effects }) { - this.down(opts) - } -} diff --git a/sdk/lib/inits/migrations/setupMigrations.ts b/sdk/lib/inits/migrations/setupMigrations.ts deleted file mode 100644 index 288b2b9d7..000000000 --- a/sdk/lib/inits/migrations/setupMigrations.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { EmVer } from "../../emverLite/mod" -import { SDKManifest } from "../../manifest/ManifestTypes" -import { ExpectedExports } from "../../types" -import { once } from "../../util/once" -import { Migration } from "./Migration" - -export class Migrations { - private constructor( - readonly manifest: SDKManifest, - readonly migrations: Array>, - ) {} - private sortedMigrations = once(() => { - const migrationsAsVersions = ( - this.migrations as Array> - ).map((x) => [EmVer.parse(x.options.version), x] as const) - migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) - return migrationsAsVersions - }) - private currentVersion = once(() => EmVer.parse(this.manifest.version)) - static of< - Manifest extends SDKManifest, - Store, - Migrations extends Array>, - >(manifest: SDKManifest, ...migrations: EnsureUniqueId) { - return new Migrations( - manifest, - migrations as Array>, - ) - } - async init({ - effects, - previousVersion, - }: Parameters[0]) { - if (!!previousVersion) { - const previousVersionEmVer = EmVer.parse(previousVersion) - for (const [_, migration] of this.sortedMigrations() - .filter((x) => x[0].greaterThan(previousVersionEmVer)) - .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.up({ effects }) - } - } - } - async uninit({ - effects, - nextVersion, - }: Parameters[0]) { - if (!!nextVersion) { - const nextVersionEmVer = EmVer.parse(nextVersion) - const reversed = [...this.sortedMigrations()].reverse() - for (const [_, migration] of reversed - .filter((x) => x[0].greaterThan(nextVersionEmVer)) - .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.down({ effects }) - } - } - } -} - -export function setupMigrations< - Manifest extends SDKManifest, - Store, - Migrations extends Array>, ->(manifest: SDKManifest, ...migrations: EnsureUniqueId) { - return Migrations.of(manifest, ...migrations) -} - -// prettier-ignore -export type EnsureUniqueId = - B extends [] ? A : - B extends [Migration, ...infer Rest] ? ( - id extends ids ? "One of the ids are not unique"[] : - EnsureUniqueId - ) : "There exists a migration that is not a Migration"[] diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index 03a7085c5..5fd1c481c 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,30 +1,42 @@ import { DependenciesReceipt } from "../config/setupConfig" +import { ExtendedVersion, VersionRange } from "../exver" import { SetInterfaces } from "../interfaces/setupInterfaces" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ExposedStorePaths } from "../store/setupExposeStore" -import { Effects, ExpectedExports } from "../types" -import { Migrations } from "./migrations/setupMigrations" +import * as T from "../types" +import { VersionGraph } from "../version/VersionGraph" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( - migrations: Migrations, +export function setupInit( + versions: VersionGraph, install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, setDependencies: (options: { - effects: Effects + effects: T.Effects input: any }) => Promise, exposedStore: ExposedStorePaths, ): { - init: ExpectedExports.init - uninit: ExpectedExports.uninit + init: T.ExpectedExports.init + uninit: T.ExpectedExports.uninit } { return { init: async (opts) => { - await migrations.init(opts) - await install.init(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, + }) + } await setInterfaces({ ...opts, input: null, @@ -33,8 +45,18 @@ export function setupInit( await setDependencies({ effects: opts.effects, input: null }) }, uninit: async (opts) => { - await migrations.uninit(opts) - await uninstall.uninit(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) + } }, } } diff --git a/sdk/lib/inits/setupInstall.ts b/sdk/lib/inits/setupInstall.ts index 3990be0ca..ab21380a0 100644 --- a/sdk/lib/inits/setupInstall.ts +++ b/sdk/lib/inits/setupInstall.ts @@ -1,29 +1,24 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type InstallFn = (opts: { - effects: Effects +export type InstallFn = (opts: { + effects: T.Effects }) => Promise -export class Install { +export class Install { private constructor(readonly fn: InstallFn) {} - static of( + static of( fn: InstallFn, ) { return new Install(fn) } - async init({ - effects, - previousVersion, - }: Parameters[0]) { - if (!previousVersion) - await this.fn({ - effects, - }) + async install({ effects }: Parameters[0]) { + await this.fn({ + effects, + }) } } -export function setupInstall( +export function setupInstall( fn: InstallFn, ) { return Install.of(fn) diff --git a/sdk/lib/inits/setupUninstall.ts b/sdk/lib/inits/setupUninstall.ts index 812848c8f..918f417e5 100644 --- a/sdk/lib/inits/setupUninstall.ts +++ b/sdk/lib/inits/setupUninstall.ts @@ -1,21 +1,20 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type UninstallFn = (opts: { - effects: Effects +export type UninstallFn = (opts: { + effects: T.Effects }) => Promise -export class Uninstall { +export class Uninstall { private constructor(readonly fn: UninstallFn) {} - static of( + static of( fn: UninstallFn, ) { return new Uninstall(fn) } - async uninit({ + async uninstall({ effects, nextVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!nextVersion) await this.fn({ effects, @@ -23,7 +22,7 @@ export class Uninstall { } } -export function setupUninstall( +export function setupUninstall( fn: UninstallFn, ) { return Uninstall.of(fn) diff --git a/sdk/lib/interfaces/Origin.ts b/sdk/lib/interfaces/Origin.ts index 52afe1ed3..cc84728ec 100644 --- a/sdk/lib/interfaces/Origin.ts +++ b/sdk/lib/interfaces/Origin.ts @@ -47,7 +47,6 @@ export class Origin { name, description, hasPrimary, - disabled, id, type, username, @@ -69,7 +68,6 @@ export class Origin { name, description, hasPrimary, - disabled, addressInfo, type, masked, diff --git a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts index 14eaee1d3..49d8020d6 100644 --- a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts +++ b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts @@ -21,7 +21,6 @@ export class ServiceInterfaceBuilder { id: string description: string hasPrimary: boolean - disabled: boolean type: ServiceInterfaceType username: string | null path: string diff --git a/sdk/lib/interfaces/setupInterfaces.ts b/sdk/lib/interfaces/setupInterfaces.ts index 5ad8d8a7d..c82b69e0b 100644 --- a/sdk/lib/interfaces/setupInterfaces.ts +++ b/sdk/lib/interfaces/setupInterfaces.ts @@ -1,17 +1,17 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { AddressInfo, Effects } from "../types" + +import * as T from "../types" import { AddressReceipt } from "./AddressReceipt" -export type InterfacesReceipt = Array +export type InterfacesReceipt = Array export type SetInterfaces< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, -> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise +> = (opts: { effects: T.Effects; input: null | ConfigInput }) => Promise export type SetupInterfaces = < - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 264574f7c..8a0505f68 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,27 +1,40 @@ +import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" -import { MountOptions, Overlay } from "../util/Overlay" + +import * as T from "../types" +import { asError } from "../util/asError" +import { + ExecSpawnable, + MountOptions, + SubContainerHandle, + SubContainer, +} from "../util/SubContainer" import { splitCommand } from "../util/splitCommand" -import { cpExecFile, cpExec } from "./Daemons" +import * as cp from "child_process" export class CommandController { private constructor( readonly runningAnswer: Promise, - readonly overlay: Overlay, - readonly pid: number | undefined, + private state: { exited: boolean }, + private readonly subcontainer: SubContainer, + private process: cp.ChildProcessWithoutNullStreams, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} - static of() { + static of() { return async ( - effects: Effects, - imageId: { - id: keyof Manifest["images"] & ImageId - sharedRun?: boolean - }, - command: ValidIfNoStupidEscape | [string, ...string[]], + effects: T.Effects, + subcontainer: + | { + id: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + } + | SubContainer, + command: T.CommandType, options: { + // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms + sigtermTimeout?: number mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay + runAsInit?: boolean env?: | { [variable: string]: string @@ -34,115 +47,93 @@ export class CommandController { }, ) => { const commands = splitCommand(command) - const overlay = options.overlay || (await Overlay.of(effects, imageId)) - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) + const subc = + subcontainer instanceof SubContainer + ? subcontainer + : await (async () => { + const subc = await SubContainer.of(effects, subcontainer) + for (let mount of options.mounts || []) { + await subc.mount(mount.options, mount.path) + } + return subc + })() + let childProcess: cp.ChildProcessWithoutNullStreams + if (options.runAsInit) { + childProcess = await subc.launch(commands, { + env: options.env, + }) + } else { + childProcess = await subc.spawn(commands, { + env: options.env, + }) } - const childProcess = await overlay.spawn(commands, { - env: options.env, - }) + const state = { exited: false } const answer = new Promise((resolve, reject) => { - childProcess.stdout.on( - "data", - options.onStdout ?? - ((data: any) => { - console.log(data.toString()) - }), - ) - childProcess.stderr.on( - "data", - options.onStderr ?? - ((data: any) => { - console.error(data.toString()) - }), - ) - - childProcess.on("exit", (code: any) => { - if (code === 0) { + childProcess.on("exit", (code) => { + state.exited = true + if ( + code === 0 || + code === 143 || + (code === null && childProcess.signalCode == "SIGTERM") + ) { return resolve(null) } - return reject(new Error(`${commands[0]} exited with code ${code}`)) + if (code) { + return reject(new Error(`${commands[0]} exited with code ${code}`)) + } else { + return reject( + new Error( + `${commands[0]} exited with signal ${childProcess.signalCode}`, + ), + ) + } }) }) - const pid = childProcess.pid - - return new CommandController(answer, overlay, pid) + return new CommandController( + answer, + state, + subc, + childProcess, + options.sigtermTimeout, + ) } } - async wait() { + 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.pid !== undefined) { - await cpExecFile("pkill", ["-9", "-s", String(this.pid)]).catch( - (_) => {}, - ) + if (!this.state.exited) { + this.process.kill("SIGKILL") } - await this.overlay.destroy().catch((_) => {}) + await this.subcontainer.destroy?.().catch((_) => {}) } } - async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { - if (this.pid === undefined) return + async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { try { - await cpExecFile("pkill", [ - `-${signal.replace("SIG", "")}`, - "-s", - String(this.pid), - ]) + if (!this.state.exited) { + if (!this.process.kill(signal)) { + console.error( + `failed to send signal ${signal} to pid ${this.process.pid}`, + ) + } + } - const didTimeout = await waitSession(this.pid, timeout) - if (didTimeout) { - await cpExecFile("pkill", [`-9`, "-s", String(this.pid)]).catch( - (_) => {}, - ) + if (signal !== "SIGKILL") { + setTimeout(() => { + this.process.kill("SIGKILL") + }, timeout) } + await this.runningAnswer } finally { - await this.overlay.destroy() + await this.subcontainer.destroy?.() } } } - -function waitSession( - sid: number, - timeout = NO_TIMEOUT, - interval = 100, -): Promise { - let nextInterval = interval * 2 - if (timeout >= 0 && timeout < nextInterval) { - nextInterval = timeout - } - let nextTimeout = timeout - if (timeout > 0) { - if (timeout >= interval) { - nextTimeout -= interval - } else { - nextTimeout = 0 - } - } - return new Promise((resolve, reject) => { - let next: NodeJS.Timeout | null = null - if (timeout !== 0) { - next = setTimeout(() => { - waitSession(sid, nextTimeout, nextInterval).then(resolve, reject) - }, interval) - } - cpExecFile("ps", [`--sid=${sid}`, "-o", "--pid="]).then( - (_) => { - if (timeout === 0) { - resolve(true) - } - }, - (e) => { - if (next) { - clearTimeout(next) - } - if (typeof e === "object" && e && "code" in e && e.code) { - resolve(false) - } else { - reject(e) - } - }, - ) - }) -} diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index c48865f94..87a7d705d 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -1,6 +1,6 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" -import { MountOptions, Overlay } from "../util/Overlay" +import * as T from "../types" +import { asError } from "../util/asError" +import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer" import { CommandController } from "./CommandController" const TIMEOUT_INCREMENT_MS = 1000 @@ -13,18 +13,22 @@ const MAX_TIMEOUT_MS = 30000 export class Daemon { private commandController: CommandController | null = null private shouldBeRunning = false - private constructor(private startCommand: () => Promise) {} - static of() { + constructor(private startCommand: () => Promise) {} + get subContainerHandle(): undefined | ExecSpawnable { + return this.commandController?.subContainerHandle + } + static of() { return async ( - effects: Effects, - imageId: { - id: keyof Manifest["images"] & ImageId - sharedRun?: boolean - }, - command: ValidIfNoStupidEscape | [string, ...string[]], + effects: T.Effects, + subcontainer: + | { + id: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + } + | SubContainer, + command: T.CommandType, options: { mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay env?: | { [variable: string]: string @@ -34,14 +38,19 @@ export class Daemon { user?: string | undefined onStdout?: (x: Buffer) => void onStderr?: (x: Buffer) => void + sigtermTimeout?: number }, ) => { const startCommand = () => - CommandController.of()(effects, imageId, command, options) + CommandController.of()( + effects, + subcontainer, + command, + options, + ) return new Daemon(startCommand) } } - async start() { if (this.commandController) { return @@ -57,7 +66,7 @@ export class Daemon { timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter) } }).catch((err) => { - console.error(err) + console.error(asError(err)) }) } async term(termOptions?: { @@ -72,8 +81,8 @@ export class Daemon { }) { this.shouldBeRunning = false await this.commandController - ?.term(termOptions) - .catch((e) => console.error(e)) + ?.term({ ...termOptions }) + .catch((e) => console.error(asError(e))) this.commandController = null } } diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 059d148ab..1ecec28d3 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,18 +1,18 @@ import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" -import { CheckResult } from "../health/checkFns" -import { SDKManifest } from "../manifest/ManifestTypes" +import { HealthCheckResult } from "../health/checkFns" + import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" -import { - DaemonReturned, - Effects, - ImageId, - ValidIfNoStupidEscape, -} from "../types" +import * as T from "../types" import { Mounts } from "./Mounts" -import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" +import { + CommandOptions, + ExecSpawnable, + MountOptions, + SubContainer, +} from "../util/SubContainer" import { splitCommand } from "../util/splitCommand" import { promisify } from "node:util" @@ -28,27 +28,30 @@ export const cpExec = promisify(CP.exec) export const cpExecFile = promisify(CP.execFile) export type Ready = { display: string | null - fn: () => Promise | CheckResult + fn: ( + spawnable: ExecSpawnable, + ) => Promise | HealthCheckResult trigger?: Trigger } type DaemonsParams< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Ids extends string, Command extends string, Id extends string, > = { - command: ValidIfNoStupidEscape | [string, ...string[]] - image: { id: keyof Manifest["images"] & ImageId; sharedRun?: boolean } + command: T.CommandType + image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean } mounts: Mounts env?: Record ready: Ready requires: Exclude[] + sigtermTimeout?: number } type ErrorDuplicateId = `The id '${Id}' is already used` -export const runCommand = () => +export const runCommand = () => CommandController.of() /** @@ -74,9 +77,9 @@ Daemons.of({ }) ``` */ -export class Daemons { +export class Daemons { private constructor( - readonly effects: Effects, + readonly effects: T.Effects, readonly started: (onTerm: () => PromiseLike) => PromiseLike, readonly daemons: Promise[], readonly ids: Ids[], @@ -92,8 +95,8 @@ export class Daemons { * @param config * @returns */ - static of(config: { - effects: Effects + static of(config: { + effects: T.Effects started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] }) { @@ -136,6 +139,7 @@ export class Daemons { this.ids, options.ready, this.effects, + options.sigtermTimeout, ) const daemons = this.daemons.concat(daemon) const ids = [...this.ids, id] as (Ids | Id)[] diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts index 48f3fab55..7ace3ed7b 100644 --- a/sdk/lib/mainFn/HealthDaemon.ts +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -1,8 +1,10 @@ -import { CheckResult } from "../health/checkFns" +import { HealthCheckResult } from "../health/checkFns" import { defaultTrigger } from "../trigger/defaultTrigger" import { Ready } from "./Daemons" import { Daemon } from "./Daemon" -import { Effects } from "../types" +import { Effects, SetHealth } from "../types" +import { DEFAULT_SIGTERM_TIMEOUT } from "." +import { asError } from "../util/asError" const oncePromise = () => { let resolve: (value: T) => void @@ -20,18 +22,18 @@ const oncePromise = () => { * */ export class HealthDaemon { - #health: CheckResult = { status: "starting", message: null } - #healthWatchers: Array<() => unknown> = [] - #running = false - #hadSuccess = false + private _health: HealthCheckResult = { result: "starting", message: null } + private healthWatchers: Array<() => unknown> = [] + private running = false constructor( - readonly daemon: Promise, + private readonly daemon: Promise, readonly daemonIndex: number, - readonly dependencies: HealthDaemon[], + private readonly dependencies: HealthDaemon[], readonly id: string, readonly ids: string[], readonly ready: Ready, readonly effects: Effects, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) { this.updateStatus() this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) @@ -42,26 +44,31 @@ export class HealthDaemon { signal?: NodeJS.Signals | undefined timeout?: number | undefined }) { - this.#healthWatchers = [] - this.#running = false - this.#healthCheckCleanup?.() + this.healthWatchers = [] + this.running = false + this.healthCheckCleanup?.() - await this.daemon.then((d) => d.stop(termOptions)) + 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) + this.healthWatchers.push(watcher) } get health() { - return Object.freeze(this.#health) + return Object.freeze(this._health) } private async changeRunning(newStatus: boolean) { - if (this.#running === newStatus) return + if (this.running === newStatus) return - this.#running = newStatus + this.running = newStatus if (newStatus) { ;(await this.daemon).start() @@ -70,19 +77,18 @@ export class HealthDaemon { ;(await this.daemon).stop() this.turnOffHealthCheck() - this.setHealth({ status: "starting", message: null }) + this.setHealth({ result: "starting", message: null }) } } - #healthCheckCleanup: (() => void) | null = null + private healthCheckCleanup: (() => void) | null = null private turnOffHealthCheck() { - this.#healthCheckCleanup?.() + this.healthCheckCleanup?.() } private async setupHealthCheck() { - if (this.#healthCheckCleanup) return + if (this.healthCheckCleanup) return const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ - hadSuccess: this.#hadSuccess, - lastResult: this.#health.status, + lastResult: this._health.result, })) const { promise: status, resolve: setStatus } = oncePromise<{ @@ -94,59 +100,51 @@ export class HealthDaemon { !res.done; res = await Promise.race([status, trigger.next()]) ) { - const response: CheckResult = await Promise.resolve( - this.ready.fn(), - ).catch((err) => { - console.error(err) - return { - status: "failure", - message: "message" in err ? err.message : String(err), - } - }) - this.setHealth(response) - if (response.status === "success") { - this.#hadSuccess = true + 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), + } + }) + 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 = () => { + this.healthCheckCleanup = () => { setStatus({ done: true }) - this.#healthCheckCleanup = null + this.healthCheckCleanup = null } } - private setHealth(health: CheckResult) { - this.#health = health - this.#healthWatchers.forEach((watcher) => watcher()) + private async setHealth(health: HealthCheckResult) { + this._health = health + this.healthWatchers.forEach((watcher) => watcher()) const display = this.ready.display - const status = health.status + const result = health.result if (!display) { return } - if ( - status === "success" || - status === "disabled" || - status === "starting" - ) { - this.effects.setHealth({ - result: status, - message: health.message, - id: this.id, - name: display, - }) - } else { - this.effects.setHealth({ - result: health.status, - message: health.message || "", - id: this.id, - name: display, - }) - } + 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.status === "success")) + const healths = this.dependencies.map((d) => d._health) + this.changeRunning(healths.every((x) => x.result === "success")) } } diff --git a/sdk/lib/mainFn/Mounts.ts b/sdk/lib/mainFn/Mounts.ts index eeedc79c6..bd947b759 100644 --- a/sdk/lib/mainFn/Mounts.ts +++ b/sdk/lib/mainFn/Mounts.ts @@ -1,10 +1,9 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects } from "../types" -import { MountOptions } from "../util/Overlay" +import * as T from "../types" +import { MountOptions } from "../util/SubContainer" type MountArray = { path: string; options: MountOptions }[] -export class Mounts { +export class Mounts { private constructor( readonly volumes: { id: Manifest["volumes"][number] @@ -26,7 +25,7 @@ export class Mounts { }[], ) {} - static of() { + static of() { return new Mounts([], [], []) } @@ -58,7 +57,7 @@ export class Mounts { return this } - addDependency( + addDependency( dependencyId: keyof Manifest["dependencies"] & string, volumeId: DependencyManifest["volumes"][number], subpath: string | null, diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index 3da57d32f..7a094a31a 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -1,12 +1,13 @@ -import { ExpectedExports } from "../types" +import * as T from "../types" import { Daemons } from "./Daemons" import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/Origin" import "./Daemons" -import { SDKManifest } from "../manifest/ManifestTypes" + import { MainEffects } from "../StartSdk" +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 @@ -17,12 +18,12 @@ import { MainEffects } from "../StartSdk" * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, -): ExpectedExports.main => { +): T.ExpectedExports.main => { return async (options) => { const result = await fn(options) return result diff --git a/sdk/lib/manifest/ManifestTypes.ts b/sdk/lib/manifest/ManifestTypes.ts index c820930c8..cc564de2d 100644 --- a/sdk/lib/manifest/ManifestTypes.ts +++ b/sdk/lib/manifest/ManifestTypes.ts @@ -1,31 +1,17 @@ -import { ValidEmVer } from "../emverLite/mod" -import { ActionMetadata, ImageConfig, ImageId } from "../types" - -export interface Container { - /** This should be pointing to a docker container name */ - image: string - /** These should match the manifest data volumes */ - mounts: Record - /** Default is 64mb */ - shmSizeMb?: `${number}${"mb" | "gb" | "b" | "kb"}` - /** if more than 30s to shutdown */ - sigtermTimeout?: `${number}${"s" | "m" | "h"}` -} - -export type ManifestVersion = ValidEmVer +import { ValidateExVer, ValidateExVers } from "../exver" +import { + ActionMetadata, + HardwareRequirements, + ImageConfig, + ImageId, + ImageSource, +} from "../types" export type SDKManifest = { /** The package identifier used by the OS. This must be unique amongst all other known packages */ readonly id: string /** A human readable service title */ readonly title: string - /** 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 - */ - readonly version: ManifestVersion - /** Release notes for the update - can be a string, paragraph or URL */ - readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ readonly license: string // name of license /** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), @@ -50,36 +36,49 @@ export type SDKManifest = { } /** Defines the os images needed to run the container processes */ - readonly images: Record + readonly images: Record /** This denotes readonly asset directories that should be available to mount to the container. - * Assuming that there will be three files with names along the lines: - * icon.* : the icon that will be this packages icon on the ui - * LICENSE : What the license is for this service - * Instructions : to be seen in the ui section of the package - * */ + * These directories are expected to be found in `assets/` at pack time. + **/ readonly assets: string[] /** This denotes any data volumes that should be available to mount to the container */ readonly volumes: string[] - readonly alerts: { - readonly install: string | null - readonly update: string | null - readonly uninstall: string | null - readonly restore: string | null - readonly start: string | null - readonly stop: string | null + readonly alerts?: { + readonly install?: string | null + readonly update?: string | null + readonly uninstall?: string | null + readonly restore?: string | null + readonly start?: string | null + readonly stop?: string | null } + readonly hasConfig?: boolean readonly dependencies: Readonly> + readonly hardwareRequirements?: { + readonly device?: { display?: RegExp; processor?: RegExp } + readonly ram?: number | null + readonly arch?: string[] | null + } } -export interface ManifestDependency { +export type SDKImageConfig = { + source: Exclude + arch?: string[] + emulateMissingAs?: string | null +} + +export type ManifestDependency = { /** * A human readable explanation on what the dependency is used for */ - description: string | null + readonly description: string | null /** * Determines if the dependency is optional or not. Times that optional that are good include such situations * such as being able to toggle other services or to use a different service for the same purpose. */ - optional: boolean + readonly optional: boolean + /** + * A url or local path for an s9pk that satisfies this dependency + */ + readonly s9pk: string } diff --git a/sdk/lib/manifest/setupManifest.ts b/sdk/lib/manifest/setupManifest.ts index 8bd39a7aa..10aaa03db 100644 --- a/sdk/lib/manifest/setupManifest.ts +++ b/sdk/lib/manifest/setupManifest.ts @@ -1,21 +1,84 @@ +import * as T from "../types" import { ImageConfig, ImageId, VolumeId } from "../osBindings" -import { SDKManifest, ManifestVersion } from "./ManifestTypes" +import { SDKManifest, SDKImageConfig } from "./ManifestTypes" +import { SDKVersion } from "../StartSdk" +import { VersionGraph } from "../version/VersionGraph" +/** + * This is an example of a function that takes a manifest and returns a new manifest with additional properties + * @param manifest Manifests are the description of the package + * @returns The manifest with additional properties + */ export function setupManifest< Id extends string, - Version extends ManifestVersion, + Version extends string, Dependencies extends Record, VolumesTypes extends VolumeId, AssetTypes extends VolumeId, ImagesTypes extends ImageId, - Manifest extends SDKManifest & { + Manifest extends { dependencies: Dependencies id: Id - version: Version assets: AssetTypes[] - images: Record + images: Record volumes: VolumesTypes[] }, ->(manifest: Manifest): Manifest { - return manifest + Satisfies extends string[] = [], +>( + 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[0] || null + images[k] = v as ImageConfig + return images + }, + {} as { [k: string]: ImageConfig }, + ) + return { + ...manifest, + gitHash: null, + 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, + }, + hasConfig: manifest.hasConfig === undefined ? true : manifest.hasConfig, + hardwareRequirements: { + device: Object.fromEntries( + Object.entries(manifest.hardwareRequirements?.device || {}).map( + ([k, v]) => [k, v.source], + ), + ), + ram: manifest.hardwareRequirements?.ram || null, + arch: + manifest.hardwareRequirements?.arch === undefined + ? Object.values(images).reduce( + (arch, config) => { + if (config.emulateMissingAs) { + return arch + } + if (arch === null) { + return config.arch + } + return arch.filter((a) => config.arch.includes(a)) + }, + null as string[] | null, + ) + : manifest.hardwareRequirements?.arch, + }, + } } diff --git a/sdk/lib/osBindings/AddAssetParams.ts b/sdk/lib/osBindings/AddAssetParams.ts index ffd7db675..7522a1cb4 100644 --- a/sdk/lib/osBindings/AddAssetParams.ts +++ b/sdk/lib/osBindings/AddAssetParams.ts @@ -1,10 +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 { Blake3Commitment } from "./Blake3Commitment" -import type { Version } from "./Version" export type AddAssetParams = { - version: Version + version: string platform: string url: string signature: AnySignature diff --git a/sdk/lib/osBindings/AddVersionParams.ts b/sdk/lib/osBindings/AddVersionParams.ts index 4ecbb7dcc..9fc281a6f 100644 --- a/sdk/lib/osBindings/AddVersionParams.ts +++ b/sdk/lib/osBindings/AddVersionParams.ts @@ -1,8 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" export type AddVersionParams = { - version: Version + version: string headline: string releaseNotes: string sourceVersion: string diff --git a/sdk/lib/osBindings/ApiState.ts b/sdk/lib/osBindings/ApiState.ts new file mode 100644 index 000000000..c3a43828a --- /dev/null +++ b/sdk/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/lib/osBindings/Callback.ts b/sdk/lib/osBindings/CallbackId.ts similarity index 76% rename from sdk/lib/osBindings/Callback.ts rename to sdk/lib/osBindings/CallbackId.ts index 1e5cb1af5..0ac5d7ce2 100644 --- a/sdk/lib/osBindings/Callback.ts +++ b/sdk/lib/osBindings/CallbackId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Callback = () => void +export type CallbackId = number diff --git a/sdk/lib/osBindings/CheckDependenciesParam.ts b/sdk/lib/osBindings/CheckDependenciesParam.ts index 54580a7ff..3a00faf4f 100644 --- a/sdk/lib/osBindings/CheckDependenciesParam.ts +++ b/sdk/lib/osBindings/CheckDependenciesParam.ts @@ -1,4 +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 | null } +export type CheckDependenciesParam = { packageIds?: Array } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts index c102c733a..a435ff87f 100644 --- a/sdk/lib/osBindings/CheckDependenciesResult.ts +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -1,11 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HealthCheckResult } from "./HealthCheckResult" +import type { HealthCheckId } from "./HealthCheckId" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" import type { PackageId } from "./PackageId" export type CheckDependenciesResult = { packageId: PackageId - isInstalled: boolean + title: string | null + installedVersion: string | null + satisfies: string[] isRunning: boolean - healthChecks: Array - version: string | null + configSatisfied: boolean + healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult } } diff --git a/sdk/lib/osBindings/ChrootParams.ts b/sdk/lib/osBindings/ChrootParams.ts deleted file mode 100644 index 19131b224..000000000 --- a/sdk/lib/osBindings/ChrootParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChrootParams = { - env: string | null - workdir: string | null - user: string | null - path: string - command: string - args: string[] -} diff --git a/sdk/lib/osBindings/CreateOverlayedImageParams.ts b/sdk/lib/osBindings/CreateSubcontainerFsParams.ts similarity index 70% rename from sdk/lib/osBindings/CreateOverlayedImageParams.ts rename to sdk/lib/osBindings/CreateSubcontainerFsParams.ts index aad94f01f..729ad4240 100644 --- a/sdk/lib/osBindings/CreateOverlayedImageParams.ts +++ b/sdk/lib/osBindings/CreateSubcontainerFsParams.ts @@ -1,4 +1,4 @@ // 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 CreateOverlayedImageParams = { imageId: ImageId } +export type CreateSubcontainerFsParams = { imageId: ImageId } diff --git a/sdk/lib/osBindings/CurrentDependencyInfo.ts b/sdk/lib/osBindings/CurrentDependencyInfo.ts index de46e4b52..2096a0113 100644 --- a/sdk/lib/osBindings/CurrentDependencyInfo.ts +++ b/sdk/lib/osBindings/CurrentDependencyInfo.ts @@ -2,9 +2,8 @@ import type { DataUrl } from "./DataUrl" export type CurrentDependencyInfo = { - title: string - icon: DataUrl - registryUrl: string - versionSpec: string + title: string | null + icon: DataUrl | null + versionRange: string configSatisfied: boolean } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) diff --git a/sdk/lib/osBindings/DepInfo.ts b/sdk/lib/osBindings/DepInfo.ts index 20b9eb4cd..d635cca3b 100644 --- a/sdk/lib/osBindings/DepInfo.ts +++ b/sdk/lib/osBindings/DepInfo.ts @@ -1,3 +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 } +export type DepInfo = { + description: string | null + optional: boolean + s9pk: PathOrUrl | null +} diff --git a/sdk/lib/osBindings/DependencyMetadata.ts b/sdk/lib/osBindings/DependencyMetadata.ts new file mode 100644 index 000000000..3d56ef052 --- /dev/null +++ b/sdk/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/lib/osBindings/DependencyRequirement.ts b/sdk/lib/osBindings/DependencyRequirement.ts index d0415bee9..3b857c476 100644 --- a/sdk/lib/osBindings/DependencyRequirement.ts +++ b/sdk/lib/osBindings/DependencyRequirement.ts @@ -1,11 +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: string - healthChecks: string[] - versionSpec: string - registryUrl: string + id: PackageId + healthChecks: Array + versionRange: string } - | { kind: "exists"; id: string; versionSpec: string; registryUrl: string } + | { kind: "exists"; id: PackageId; versionRange: string } diff --git a/sdk/lib/osBindings/DestroyOverlayedImageParams.ts b/sdk/lib/osBindings/DestroySubcontainerFsParams.ts similarity index 71% rename from sdk/lib/osBindings/DestroyOverlayedImageParams.ts rename to sdk/lib/osBindings/DestroySubcontainerFsParams.ts index b5b7484a2..3f85d2217 100644 --- a/sdk/lib/osBindings/DestroyOverlayedImageParams.ts +++ b/sdk/lib/osBindings/DestroySubcontainerFsParams.ts @@ -1,4 +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 DestroyOverlayedImageParams = { guid: Guid } +export type DestroySubcontainerFsParams = { guid: Guid } diff --git a/sdk/lib/osBindings/RemoveActionParams.ts b/sdk/lib/osBindings/EchoParams.ts similarity index 69% rename from sdk/lib/osBindings/RemoveActionParams.ts rename to sdk/lib/osBindings/EchoParams.ts index c343620b8..232dfb8ab 100644 --- a/sdk/lib/osBindings/RemoveActionParams.ts +++ b/sdk/lib/osBindings/EchoParams.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type RemoveActionParams = { id: string } +export type EchoParams = { message: string } diff --git a/sdk/lib/osBindings/ExecuteAction.ts b/sdk/lib/osBindings/ExecuteAction.ts index b4eb60949..6e3c44f79 100644 --- a/sdk/lib/osBindings/ExecuteAction.ts +++ b/sdk/lib/osBindings/ExecuteAction.ts @@ -1,9 +1,9 @@ // 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 { ActionId } from "./ActionId" +import type { PackageId } from "./PackageId" export type ExecuteAction = { - procedureId: Guid - serviceId: string | null - actionId: string + packageId?: PackageId + actionId: ActionId input: any } diff --git a/sdk/lib/osBindings/ExportActionParams.ts b/sdk/lib/osBindings/ExportActionParams.ts index 5eee8fc63..8bcfbc349 100644 --- a/sdk/lib/osBindings/ExportActionParams.ts +++ b/sdk/lib/osBindings/ExportActionParams.ts @@ -1,4 +1,10 @@ // 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 { PackageId } from "./PackageId" -export type ExportActionParams = { id: string; metadata: ActionMetadata } +export type ExportActionParams = { + packageId?: PackageId + id: ActionId + metadata: ActionMetadata +} diff --git a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts index b93e83f7c..28ac89916 100644 --- a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts @@ -8,7 +8,6 @@ export type ExportServiceInterfaceParams = { name: string description: string hasPrimary: boolean - disabled: boolean masked: boolean addressInfo: AddressInfo type: ServiceInterfaceType diff --git a/sdk/lib/osBindings/FullIndex.ts b/sdk/lib/osBindings/FullIndex.ts index 4d9914015..c7889760a 100644 --- a/sdk/lib/osBindings/FullIndex.ts +++ b/sdk/lib/osBindings/FullIndex.ts @@ -6,6 +6,7 @@ import type { PackageIndex } from "./PackageIndex" import type { SignerInfo } from "./SignerInfo" export type FullIndex = { + name: string | null icon: DataUrl | null package: PackageIndex os: OsIndex diff --git a/sdk/lib/osBindings/ParamsMaybePackageId.ts b/sdk/lib/osBindings/GetConfiguredParams.ts similarity index 51% rename from sdk/lib/osBindings/ParamsMaybePackageId.ts rename to sdk/lib/osBindings/GetConfiguredParams.ts index a20bb9aa5..66fb6e320 100644 --- a/sdk/lib/osBindings/ParamsMaybePackageId.ts +++ b/sdk/lib/osBindings/GetConfiguredParams.ts @@ -1,3 +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 ParamsMaybePackageId = { packageId: string | null } +export type GetConfiguredParams = { packageId?: PackageId } diff --git a/sdk/lib/osBindings/GetHostInfoParams.ts b/sdk/lib/osBindings/GetHostInfoParams.ts index 120b4cfe1..ff6d9d709 100644 --- a/sdk/lib/osBindings/GetHostInfoParams.ts +++ b/sdk/lib/osBindings/GetHostInfoParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" export type GetHostInfoParams = { hostId: HostId - packageId: string | null - callback: Callback + packageId?: PackageId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetOsAssetParams.ts b/sdk/lib/osBindings/GetOsAssetParams.ts index 9872d0b59..100f711c7 100644 --- a/sdk/lib/osBindings/GetOsAssetParams.ts +++ b/sdk/lib/osBindings/GetOsAssetParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type GetOsAssetParams = { version: Version; platform: string } +export type GetOsAssetParams = { version: string; platform: string } diff --git a/sdk/lib/osBindings/GetVersionParams.ts b/sdk/lib/osBindings/GetOsVersionParams.ts similarity index 85% rename from sdk/lib/osBindings/GetVersionParams.ts rename to sdk/lib/osBindings/GetOsVersionParams.ts index 853d76022..de0458645 100644 --- a/sdk/lib/osBindings/GetVersionParams.ts +++ b/sdk/lib/osBindings/GetOsVersionParams.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GetVersionParams = { +export type GetOsVersionParams = { source: string | null target: string | null serverId: string | null diff --git a/sdk/lib/osBindings/GetPackageParams.ts b/sdk/lib/osBindings/GetPackageParams.ts index 8b852c35c..3dde55b28 100644 --- a/sdk/lib/osBindings/GetPackageParams.ts +++ b/sdk/lib/osBindings/GetPackageParams.ts @@ -7,5 +7,5 @@ export type GetPackageParams = { id: PackageId | null version: string | null sourceVersion: Version | null - otherVersions: PackageDetailLevel | null + otherVersions: PackageDetailLevel } diff --git a/sdk/lib/osBindings/GetPrimaryUrlParams.ts b/sdk/lib/osBindings/GetPrimaryUrlParams.ts index dbafa4152..06bf73976 100644 --- a/sdk/lib/osBindings/GetPrimaryUrlParams.ts +++ b/sdk/lib/osBindings/GetPrimaryUrlParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" -import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { CallbackId } from "./CallbackId" +import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" export type GetPrimaryUrlParams = { - packageId: string | null - serviceInterfaceId: ServiceInterfaceId - callback: Callback + packageId?: PackageId + hostId: HostId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetServiceInterfaceParams.ts b/sdk/lib/osBindings/GetServiceInterfaceParams.ts index 0a8bdfcb2..b71591e17 100644 --- a/sdk/lib/osBindings/GetServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/GetServiceInterfaceParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" import type { ServiceInterfaceId } from "./ServiceInterfaceId" export type GetServiceInterfaceParams = { - packageId: string | null + packageId?: PackageId serviceInterfaceId: ServiceInterfaceId - callback: Callback + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetServicePortForwardParams.ts b/sdk/lib/osBindings/GetServicePortForwardParams.ts index beb423d9a..63236328e 100644 --- a/sdk/lib/osBindings/GetServicePortForwardParams.ts +++ b/sdk/lib/osBindings/GetServicePortForwardParams.ts @@ -1,8 +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: string | null - internalPort: number + packageId?: PackageId hostId: HostId + internalPort: number } diff --git a/sdk/lib/osBindings/GetSslCertificateParams.ts b/sdk/lib/osBindings/GetSslCertificateParams.ts index a33eff540..85c677540 100644 --- a/sdk/lib/osBindings/GetSslCertificateParams.ts +++ b/sdk/lib/osBindings/GetSslCertificateParams.ts @@ -1,8 +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 = { - packageId: string | null - hostId: string - algorithm: Algorithm | null + hostnames: string[] + algorithm?: Algorithm + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetSslKeyParams.ts b/sdk/lib/osBindings/GetSslKeyParams.ts index 0438c345a..2ca3076c8 100644 --- a/sdk/lib/osBindings/GetSslKeyParams.ts +++ b/sdk/lib/osBindings/GetSslKeyParams.ts @@ -1,8 +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 = { - packageId: string | null - hostId: string - algorithm: Algorithm | null -} +export type GetSslKeyParams = { hostnames: string[]; algorithm?: Algorithm } diff --git a/sdk/lib/osBindings/GetStoreParams.ts b/sdk/lib/osBindings/GetStoreParams.ts index dc3d4e211..e134cd4a6 100644 --- a/sdk/lib/osBindings/GetStoreParams.ts +++ b/sdk/lib/osBindings/GetStoreParams.ts @@ -1,3 +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: string | null; path: string } +export type GetStoreParams = { + packageId?: PackageId + path: string + callback?: CallbackId +} diff --git a/sdk/lib/osBindings/GetSystemSmtpParams.ts b/sdk/lib/osBindings/GetSystemSmtpParams.ts index 650d59c49..73b91057c 100644 --- a/sdk/lib/osBindings/GetSystemSmtpParams.ts +++ b/sdk/lib/osBindings/GetSystemSmtpParams.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" -export type GetSystemSmtpParams = { callback: Callback } +export type GetSystemSmtpParams = { callback: CallbackId | null } diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index 0e1da1f36..e17568eec 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type HardwareRequirements = { - device: { [key: string]: string } + device: { display?: string; processor?: string } ram: number | null arch: string[] | null } diff --git a/sdk/lib/osBindings/HostAddress.ts b/sdk/lib/osBindings/HostAddress.ts index 0388e49c7..73b46d8e5 100644 --- a/sdk/lib/osBindings/HostAddress.ts +++ b/sdk/lib/osBindings/HostAddress.ts @@ -1,3 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HostAddress = { kind: "onion"; address: string } +export type HostAddress = + | { kind: "onion"; address: string } + | { kind: "domain"; address: string } diff --git a/sdk/lib/osBindings/InstallParams.ts b/sdk/lib/osBindings/InstallParams.ts new file mode 100644 index 000000000..2b70ad593 --- /dev/null +++ b/sdk/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/lib/osBindings/ListServiceInterfacesParams.ts b/sdk/lib/osBindings/ListServiceInterfacesParams.ts index 4140831d0..fd27ace2b 100644 --- a/sdk/lib/osBindings/ListServiceInterfacesParams.ts +++ b/sdk/lib/osBindings/ListServiceInterfacesParams.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" export type ListServiceInterfacesParams = { - packageId: string | null - callback: Callback + packageId?: PackageId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/ListVersionSignersParams.ts b/sdk/lib/osBindings/ListVersionSignersParams.ts index d066fbeb4..baf516bf2 100644 --- a/sdk/lib/osBindings/ListVersionSignersParams.ts +++ b/sdk/lib/osBindings/ListVersionSignersParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type ListVersionSignersParams = { version: Version } +export type ListVersionSignersParams = { version: string } diff --git a/sdk/lib/osBindings/LoginParams.ts b/sdk/lib/osBindings/LoginParams.ts index 272f7ed07..acaf5b8a1 100644 --- a/sdk/lib/osBindings/LoginParams.ts +++ b/sdk/lib/osBindings/LoginParams.ts @@ -1,4 +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; metadata: any } +export type LoginParams = { + password: PasswordType | null + ephemeral: boolean + metadata: any +} diff --git a/sdk/lib/osBindings/MainStatus.ts b/sdk/lib/osBindings/MainStatus.ts index 1b9d8482e..1f6b3babe 100644 --- a/sdk/lib/osBindings/MainStatus.ts +++ b/sdk/lib/osBindings/MainStatus.ts @@ -1,21 +1,20 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Duration } from "./Duration" import type { HealthCheckId } from "./HealthCheckId" -import type { HealthCheckResult } from "./HealthCheckResult" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" +import type { StartStop } from "./StartStop" export type MainStatus = | { status: "stopped" } | { status: "restarting" } | { status: "restoring" } - | { status: "stopping"; timeout: Duration } - | { status: "starting" } + | { status: "stopping" } | { - status: "running" - started: string - health: { [key: HealthCheckId]: HealthCheckResult } + status: "starting" + health: { [key: HealthCheckId]: NamedHealthCheckResult } } | { - status: "backingUp" - started: string | null - health: { [key: HealthCheckId]: HealthCheckResult } + status: "running" + started: string + health: { [key: HealthCheckId]: NamedHealthCheckResult } } + | { status: "backingUp"; onComplete: StartStop } diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index d808f47a2..d40223236 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -13,7 +13,10 @@ export type Manifest = { id: PackageId title: string version: Version + satisfies: Array releaseNotes: string + canMigrateTo: string + canMigrateFrom: string license: string wrapperRepo: string upstreamRepo: string diff --git a/sdk/lib/osBindings/MountTarget.ts b/sdk/lib/osBindings/MountTarget.ts index e4888d075..bbee5453b 100644 --- a/sdk/lib/osBindings/MountTarget.ts +++ b/sdk/lib/osBindings/MountTarget.ts @@ -1,8 +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: string - volumeId: string + packageId: PackageId + volumeId: VolumeId subpath: string | null readonly: boolean } diff --git a/sdk/lib/osBindings/HealthCheckResult.ts b/sdk/lib/osBindings/NamedHealthCheckResult.ts similarity index 85% rename from sdk/lib/osBindings/HealthCheckResult.ts rename to sdk/lib/osBindings/NamedHealthCheckResult.ts index 6fa3d3f8c..c967e9b34 100644 --- a/sdk/lib/osBindings/HealthCheckResult.ts +++ b/sdk/lib/osBindings/NamedHealthCheckResult.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HealthCheckResult = { name: string } & ( +export type NamedHealthCheckResult = { name: string } & ( | { result: "success"; message: string | null } | { result: "disabled"; message: string | null } | { result: "starting"; message: string | null } diff --git a/sdk/lib/osBindings/OsIndex.ts b/sdk/lib/osBindings/OsIndex.ts index 9fb795402..fe9a4e395 100644 --- a/sdk/lib/osBindings/OsIndex.ts +++ b/sdk/lib/osBindings/OsIndex.ts @@ -1,5 +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" -import type { Version } from "./Version" +import type { OsVersionInfoMap } from "./OsVersionInfoMap" -export type OsIndex = { versions: { [key: Version]: OsVersionInfo } } +export type OsIndex = { versions: OsVersionInfoMap } diff --git a/sdk/lib/osBindings/OsVersionInfoMap.ts b/sdk/lib/osBindings/OsVersionInfoMap.ts new file mode 100644 index 000000000..6f333f1fb --- /dev/null +++ b/sdk/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/lib/osBindings/PackageDataEntry.ts b/sdk/lib/osBindings/PackageDataEntry.ts index ef805741b..41bd98bba 100644 --- a/sdk/lib/osBindings/PackageDataEntry.ts +++ b/sdk/lib/osBindings/PackageDataEntry.ts @@ -8,9 +8,11 @@ import type { PackageState } from "./PackageState" import type { ServiceInterface } from "./ServiceInterface" import type { ServiceInterfaceId } from "./ServiceInterfaceId" import type { Status } from "./Status" +import type { Version } from "./Version" export type PackageDataEntry = { stateInfo: PackageState + dataVersion: Version | null status: Status registry: string | null developerKey: string diff --git a/sdk/lib/osBindings/PackageDetailLevel.ts b/sdk/lib/osBindings/PackageDetailLevel.ts index b5e1ae42b..f2016f632 100644 --- a/sdk/lib/osBindings/PackageDetailLevel.ts +++ b/sdk/lib/osBindings/PackageDetailLevel.ts @@ -1,3 +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 = "short" | "full" +export type PackageDetailLevel = "none" | "short" | "full" diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index 364c530f2..80481acb3 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -1,8 +1,11 @@ // 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 { HardwareRequirements } from "./HardwareRequirements" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +import type { PackageId } from "./PackageId" import type { RegistryAsset } from "./RegistryAsset" export type PackageVersionInfo = { @@ -16,6 +19,9 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string + donationUrl: string | null + alerts: Alerts + dependencyMetadata: { [key: PackageId]: DependencyMetadata } osVersion: string hardwareRequirements: HardwareRequirements sourceVersion: string | null diff --git a/sdk/lib/osBindings/SetSystemSmtpParams.ts b/sdk/lib/osBindings/PathOrUrl.ts similarity index 68% rename from sdk/lib/osBindings/SetSystemSmtpParams.ts rename to sdk/lib/osBindings/PathOrUrl.ts index 49c66e86c..9c4ff1e28 100644 --- a/sdk/lib/osBindings/SetSystemSmtpParams.ts +++ b/sdk/lib/osBindings/PathOrUrl.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SetSystemSmtpParams = { smtp: string } +export type PathOrUrl = string diff --git a/sdk/lib/osBindings/Public.ts b/sdk/lib/osBindings/Public.ts index c77ae05e3..4fb186607 100644 --- a/sdk/lib/osBindings/Public.ts +++ b/sdk/lib/osBindings/Public.ts @@ -5,5 +5,5 @@ import type { ServerInfo } from "./ServerInfo" export type Public = { serverInfo: ServerInfo packageData: AllPackageData - ui: any + ui: unknown } diff --git a/sdk/lib/osBindings/RegistryAsset.ts b/sdk/lib/osBindings/RegistryAsset.ts index 3eb13e8a4..41f09431f 100644 --- a/sdk/lib/osBindings/RegistryAsset.ts +++ b/sdk/lib/osBindings/RegistryAsset.ts @@ -3,6 +3,7 @@ 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/lib/osBindings/RegistryInfo.ts b/sdk/lib/osBindings/RegistryInfo.ts new file mode 100644 index 000000000..f9265fdec --- /dev/null +++ b/sdk/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/lib/osBindings/RemoveAddressParams.ts b/sdk/lib/osBindings/RemoveAddressParams.ts deleted file mode 100644 index 14099ebbc..000000000 --- a/sdk/lib/osBindings/RemoveAddressParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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 RemoveAddressParams = { id: ServiceInterfaceId } diff --git a/sdk/lib/osBindings/RemoveVersionParams.ts b/sdk/lib/osBindings/RemoveVersionParams.ts index d00a6ee9e..2c974de56 100644 --- a/sdk/lib/osBindings/RemoveVersionParams.ts +++ b/sdk/lib/osBindings/RemoveVersionParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type RemoveVersionParams = { version: Version } +export type RemoveVersionParams = { version: string } diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts index 935e3a99f..76840cfc4 100644 --- a/sdk/lib/osBindings/ServerInfo.ts +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -2,6 +2,7 @@ import type { Governor } from "./Governor" import type { IpInfo } from "./IpInfo" import type { ServerStatus } from "./ServerStatus" +import type { SmtpValue } from "./SmtpValue" import type { WifiInfo } from "./WifiInfo" export type ServerInfo = { @@ -28,5 +29,5 @@ export type ServerInfo = { ntpSynced: boolean zram: boolean governor: Governor | null - smtp: string | null + smtp: SmtpValue | null } diff --git a/sdk/lib/osBindings/ServiceInterface.ts b/sdk/lib/osBindings/ServiceInterface.ts index 91ac77515..9bcec0056 100644 --- a/sdk/lib/osBindings/ServiceInterface.ts +++ b/sdk/lib/osBindings/ServiceInterface.ts @@ -8,7 +8,6 @@ export type ServiceInterface = { name: string description: string hasPrimary: boolean - disabled: boolean masked: boolean addressInfo: AddressInfo type: ServiceInterfaceType diff --git a/sdk/lib/osBindings/SetDataVersionParams.ts b/sdk/lib/osBindings/SetDataVersionParams.ts new file mode 100644 index 000000000..3b577d2b1 --- /dev/null +++ b/sdk/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/lib/osBindings/SignAssetParams.ts b/sdk/lib/osBindings/SignAssetParams.ts index d55a061a7..39f54ad69 100644 --- a/sdk/lib/osBindings/SignAssetParams.ts +++ b/sdk/lib/osBindings/SignAssetParams.ts @@ -1,9 +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" -import type { Version } from "./Version" export type SignAssetParams = { - version: Version + version: string platform: string signature: AnySignature } diff --git a/sdk/lib/osBindings/SmtpValue.ts b/sdk/lib/osBindings/SmtpValue.ts new file mode 100644 index 000000000..5291d6602 --- /dev/null +++ b/sdk/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/lib/osBindings/ParamsPackageId.ts b/sdk/lib/osBindings/StartStop.ts similarity index 67% rename from sdk/lib/osBindings/ParamsPackageId.ts rename to sdk/lib/osBindings/StartStop.ts index f4dd1c1eb..c8be35fb7 100644 --- a/sdk/lib/osBindings/ParamsPackageId.ts +++ b/sdk/lib/osBindings/StartStop.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ParamsPackageId = { packageId: string } +export type StartStop = "start" | "stop" diff --git a/sdk/lib/osBindings/VersionSignerParams.ts b/sdk/lib/osBindings/VersionSignerParams.ts index 102eecefd..781e2a4df 100644 --- a/sdk/lib/osBindings/VersionSignerParams.ts +++ b/sdk/lib/osBindings/VersionSignerParams.ts @@ -1,5 +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" -import type { Version } from "./Version" -export type VersionSignerParams = { version: Version; signer: Guid } +export type VersionSignerParams = { version: string; signer: Guid } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 32e57956a..9492fe796 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -15,6 +15,7 @@ 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" @@ -24,24 +25,25 @@ export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" export { Blake3Commitment } from "./Blake3Commitment" export { BlockDev } from "./BlockDev" -export { Callback } from "./Callback" +export { CallbackId } from "./CallbackId" export { Category } from "./Category" export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesResult } from "./CheckDependenciesResult" -export { ChrootParams } from "./ChrootParams" export { Cifs } from "./Cifs" export { ContactInfo } from "./ContactInfo" -export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" +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 { DestroyOverlayedImageParams } from "./DestroyOverlayedImageParams" +export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams" export { Duration } from "./Duration" +export { EchoParams } from "./EchoParams" export { EncryptedWire } from "./EncryptedWire" export { ExecuteAction } from "./ExecuteAction" export { ExportActionParams } from "./ExportActionParams" @@ -49,8 +51,10 @@ export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" export { ExposeForDependentsParams } from "./ExposeForDependentsParams" export { FullIndex } from "./FullIndex" export { FullProgress } from "./FullProgress" +export { GetConfiguredParams } from "./GetConfiguredParams" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" +export { GetOsVersionParams } from "./GetOsVersionParams" export { GetPackageParams } from "./GetPackageParams" export { GetPackageResponseFull } from "./GetPackageResponseFull" export { GetPackageResponse } from "./GetPackageResponse" @@ -61,12 +65,10 @@ export { GetSslCertificateParams } from "./GetSslCertificateParams" export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" -export { GetVersionParams } from "./GetVersionParams" export { Governor } from "./Governor" export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" export { HealthCheckId } from "./HealthCheckId" -export { HealthCheckResult } from "./HealthCheckResult" export { HostAddress } from "./HostAddress" export { HostId } from "./HostId" export { HostKind } from "./HostKind" @@ -82,6 +84,7 @@ 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 { LanInfo } from "./LanInfo" @@ -94,9 +97,11 @@ 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 { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" +export { OsVersionInfoMap } from "./OsVersionInfoMap" export { OsVersionInfo } from "./OsVersionInfo" export { PackageDataEntry } from "./PackageDataEntry" export { PackageDetailLevel } from "./PackageDetailLevel" @@ -106,16 +111,14 @@ export { PackageInfoShort } from "./PackageInfoShort" export { PackageInfo } from "./PackageInfo" export { PackageState } from "./PackageState" export { PackageVersionInfo } from "./PackageVersionInfo" -export { ParamsMaybePackageId } from "./ParamsMaybePackageId" -export { ParamsPackageId } from "./ParamsPackageId" 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 { RemoveActionParams } from "./RemoveActionParams" -export { RemoveAddressParams } from "./RemoveAddressParams" +export { RegistryInfo } from "./RegistryInfo" export { RemoveVersionParams } from "./RemoveVersionParams" export { RequestCommitment } from "./RequestCommitment" export { Security } from "./Security" @@ -129,18 +132,20 @@ export { SessionList } from "./SessionList" export { Sessions } from "./Sessions" export { Session } from "./Session" export { SetConfigured } from "./SetConfigured" +export { SetDataVersionParams } from "./SetDataVersionParams" export { SetDependenciesParams } from "./SetDependenciesParams" export { SetHealth } from "./SetHealth" export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetStoreParams } from "./SetStoreParams" -export { SetSystemSmtpParams } from "./SetSystemSmtpParams" 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 { Status } from "./Status" export { UpdatingState } from "./UpdatingState" export { VerifyCifsParams } from "./VerifyCifsParams" diff --git a/sdk/lib/s9pk/merkleArchive/varint.ts b/sdk/lib/s9pk/merkleArchive/varint.ts index 2bf4793b1..016505307 100644 --- a/sdk/lib/s9pk/merkleArchive/varint.ts +++ b/sdk/lib/s9pk/merkleArchive/varint.ts @@ -1,3 +1,5 @@ +import { asError } from "../../util/asError" + const msb = 0x80 const dropMsb = 0x7f const maxSize = Math.floor((8 * 8 + 7) / 7) @@ -38,7 +40,7 @@ export class VarIntProcessor { if (success) { return result } else { - console.error(this.buf) + console.error(asError(this.buf)) return null } } diff --git a/sdk/lib/store/getStore.ts b/sdk/lib/store/getStore.ts index 38265c7fd..5250a02a1 100644 --- a/sdk/lib/store/getStore.ts +++ b/sdk/lib/store/getStore.ts @@ -28,7 +28,6 @@ export class GetStore { return this.effects.store.get({ ...this.options, path: extractJsonPath(this.path), - callback: () => {}, }) } diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index c5003c55b..5c65271a8 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -6,6 +6,8 @@ import { Variants } from "../config/builder/variants" import { ValueSpec } from "../config/configTypes" import { setupManifest } from "../manifest/setupManifest" import { StartSdk } from "../StartSdk" +import { VersionGraph } from "../version/VersionGraph" +import { VersionInfo } from "../version/VersionInfo" describe("builder tests", () => { test("text", async () => { @@ -366,41 +368,48 @@ describe("values", () => { test("datetime", async () => { const sdk = StartSdk.of() .withManifest( - setupManifest({ - id: "testOutput", - title: "", - version: "1.0", - releaseNotes: "", - license: "", - replaces: [], - 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: { - remoteTest: { - description: "", - optional: true, + setupManifest( + VersionGraph.of( + VersionInfo.of({ + version: "1.0.0:0", + releaseNotes: "", + migrations: {}, + }), + ), + { + 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) diff --git a/sdk/lib/test/emverList.test.ts b/sdk/lib/test/emverList.test.ts deleted file mode 100644 index 07dbb5aaf..000000000 --- a/sdk/lib/test/emverList.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { EmVer, notRange, rangeAnd, rangeOf, rangeOr } from "../emverLite/mod" -describe("EmVer", () => { - { - { - const checker = rangeOf("*") - test("rangeOf('*')", () => { - checker.check("1") - checker.check("1.2") - checker.check("1.2.3") - checker.check("1.2.3.4") - checker.check("1.2.3.4.5") - checker.check("1.2.3.4.5.6") - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - test("rangeOf('*') invalid", () => { - expect(() => checker.check("a")).toThrow() - expect(() => checker.check("")).toThrow() - expect(() => checker.check("1..3")).toThrow() - }) - } - - { - const checker = rangeOf(">1.2.3.4") - test(`rangeOf(">1.2.3.4") valid`, () => { - expect(checker.check("2-beta123")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - }) - - test(`rangeOf(">1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3.4")).toEqual(false) - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("=1.2.3") - test(`rangeOf("=1.2.3") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - }) - - test(`rangeOf("=1.2.3") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.1")).toEqual(false) - expect(checker.check("1.2")).toEqual(false) - }) - } - { - const checker = rangeOf(">=1.2.3.4") - test(`rangeOf(">=1.2.3.4") valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - - test(`rangeOf(">=1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("<1.2.3.4") - test(`rangeOf("<1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - expect(checker.check("1.2.3.4")).toEqual(false) - }) - - test(`rangeOf("<1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - { - const checker = rangeOf("<=1.2.3.4") - test(`rangeOf("<=1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("<=1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - } - - { - const checkA = rangeOf(">1") - const checkB = rangeOf("<=2") - - const checker = rangeAnd(checkA, checkB) - test(`simple and(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.1")).toEqual(true) - }) - test(`simple and(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("0")).toEqual(false) - }) - } - { - const checkA = rangeOf("<1") - const checkB = rangeOf("=2") - - const checker = rangeOr(checkA, checkB) - test(`simple or(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("0.1")).toEqual(true) - }) - test(`simple or(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - }) - } - - { - const checker = rangeOf("1.2.*") - test(`rangeOf(1.2.*) valid`, () => { - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(true) - }) - test(`rangeOf(1.2.*) invalid`, () => { - expect(checker.check("1.3")).toEqual(false) - expect(checker.check("1.3.1")).toEqual(false) - - expect(checker.check("1.1.1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - - expect(checker.check("2")).toEqual(false) - }) - } - - { - const checker = notRange(rangeOf("1.2.*")) - test(`notRange(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`notRange(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - const checker = rangeOf("!1.2.*") - test(`!(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`!(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - test(`no and ranges`, () => { - expect(() => rangeAnd()).toThrow() - }) - test(`no or ranges`, () => { - expect(() => rangeOr()).toThrow() - }) - } - { - const checker = rangeOf("!>1.2.3.4") - test(`rangeOf("!>1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("!>1.2.3.4") valid`, () => { - expect(checker.check("1.2.3.4")).toEqual(true) - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - - { - test(">1 && =1.2", () => { - const checker = rangeOf(">1 && =1.2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(false) - }) - test("=1 || =2", () => { - const checker = rangeOf("=1 || =2") - - expect(checker.check("1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test(">1 && =1.2 || =2", () => { - const checker = rangeOf(">1 && =1.2 || =2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1")).toEqual(false) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { - const checker = rangeOf("<1.5 && >1 || >1.5 && <3") - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.5")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("3")).toEqual(false) - }) - - test("Compare function on the emver", () => { - const a = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - 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 = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - expect(a.compareForSort(b)).toEqual(-1) - expect(b.compareForSort(a)).toEqual(1) - expect(a.compareForSort(a)).toEqual(0) - }) - } - } -}) diff --git a/sdk/lib/test/exverList.test.ts b/sdk/lib/test/exverList.test.ts new file mode 100644 index 000000000..e29a9f0d1 --- /dev/null +++ b/sdk/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/lib/test/graph.test.ts b/sdk/lib/test/graph.test.ts new file mode 100644 index 000000000..7f02adc2e --- /dev/null +++ b/sdk/lib/test/graph.test.ts @@ -0,0 +1,148 @@ +import { Graph } from "../util/graph" + +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/lib/test/host.test.ts b/sdk/lib/test/host.test.ts index a8ae317ed..64d486a94 100644 --- a/sdk/lib/test/host.test.ts +++ b/sdk/lib/test/host.test.ts @@ -16,7 +16,6 @@ describe("host", () => { id: "foo", description: "A Foo", hasPrimary: false, - disabled: false, type: "ui", username: "bar", path: "/baz", diff --git a/sdk/lib/test/output.sdk.ts b/sdk/lib/test/output.sdk.ts index 189491be5..4cdf85111 100644 --- a/sdk/lib/test/output.sdk.ts +++ b/sdk/lib/test/output.sdk.ts @@ -1,44 +1,56 @@ import { StartSdk } from "../StartSdk" import { setupManifest } from "../manifest/setupManifest" +import { VersionInfo } from "../version/VersionInfo" +import { VersionGraph } from "../version/VersionGraph" export type Manifest = any export const sdk = StartSdk.of() .withManifest( - setupManifest({ - id: "testOutput", - title: "", - version: "1.0", - releaseNotes: "", - license: "", - replaces: [], - 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: { - remoteTest: { - description: "", - optional: false, + setupManifest( + VersionGraph.of( + VersionInfo.of({ + version: "1.0.0:0", + releaseNotes: "", + migrations: {}, + }) + .satisfies("#other:1.0.0:0") + .satisfies("#other:2.0.0:0"), + ), + { + id: "testOutput", + title: "", + license: "", + replaces: [], + 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: false, + s9pk: "https://example.com/remote-test.s9pk", + }, }, }, - }), + ), ) .withStore<{ storeRoot: { storeLeaf: "value" } }>() .build(true) diff --git a/sdk/lib/test/setupDependencyConfig.test.ts b/sdk/lib/test/setupDependencyConfig.test.ts index 5fa4a0ddf..622559eb6 100644 --- a/sdk/lib/test/setupDependencyConfig.test.ts +++ b/sdk/lib/test/setupDependencyConfig.test.ts @@ -21,7 +21,7 @@ describe("setupDependencyConfig", () => { dependencyConfig: async ({}) => {}, }) sdk.setupDependencyConfig(testConfig, { - remoteTest, + "remote-test": remoteTest, }) }) }) diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 79ec62106..bcbc4b6ec 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -2,14 +2,16 @@ import { Effects } from "../types" import { CheckDependenciesParam, ExecuteAction, + GetConfiguredParams, + GetStoreParams, + SetDataVersionParams, SetMainStatus, + SetStoreParams, } from ".././osBindings" -import { CreateOverlayedImageParams } from ".././osBindings" -import { DestroyOverlayedImageParams } from ".././osBindings" +import { CreateSubcontainerFsParams } from ".././osBindings" +import { DestroySubcontainerFsParams } from ".././osBindings" import { BindParams } from ".././osBindings" import { GetHostInfoParams } from ".././osBindings" -import { ParamsPackageId } from ".././osBindings" -import { ParamsMaybePackageId } from ".././osBindings" import { SetConfigured } from ".././osBindings" import { SetHealth } from ".././osBindings" import { ExposeForDependentsParams } from ".././osBindings" @@ -22,49 +24,59 @@ import { GetServicePortForwardParams } from ".././osBindings" import { ExportServiceInterfaceParams } from ".././osBindings" import { GetPrimaryUrlParams } from ".././osBindings" import { ListServiceInterfacesParams } from ".././osBindings" -import { RemoveAddressParams } from ".././osBindings" import { ExportActionParams } from ".././osBindings" -import { RemoveActionParams } 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`, () => { const testInput: any = {} - typeEquality<{ - [K in keyof Effects]: Effects[K] extends (args: infer A) => any - ? A - : never - }>({ + typeEquality({ executeAction: {} as ExecuteAction, - createOverlayedImage: {} as CreateOverlayedImageParams, - destroyOverlayedImage: {} as DestroyOverlayedImageParams, + subcontainer: { + createFs: {} as CreateSubcontainerFsParams, + destroyFs: {} as DestroySubcontainerFsParams, + }, clearBindings: undefined, + getInstalledPackages: undefined, bind: {} as BindParams, - getHostInfo: {} as GetHostInfoParams, - exists: {} as ParamsPackageId, - getConfigured: undefined, - stopped: {} as ParamsMaybePackageId, - running: {} as ParamsPackageId, + getHostInfo: {} as WithCallback, + getConfigured: {} as GetConfiguredParams, restart: undefined, shutdown: undefined, setConfigured: {} as SetConfigured, + setDataVersion: {} as SetDataVersionParams, + getDataVersion: undefined, setHealth: {} as SetHealth, exposeForDependents: {} as ExposeForDependentsParams, - getSslCertificate: {} as GetSslCertificateParams, + getSslCertificate: {} as WithCallback, getSslKey: {} as GetSslKeyParams, - getServiceInterface: {} as GetServiceInterfaceParams, + getServiceInterface: {} as WithCallback, setDependencies: {} as SetDependenciesParams, - store: {} as never, - getSystemSmtp: {} as GetSystemSmtpParams, + store: { + get: {} as any, // as GetStoreParams, + set: {} as any, // as SetStoreParams, + }, + getSystemSmtp: {} as WithCallback, getContainerIp: undefined, getServicePortForward: {} as GetServicePortForwardParams, clearServiceInterfaces: undefined, exportServiceInterface: {} as ExportServiceInterfaceParams, - getPrimaryUrl: {} as GetPrimaryUrlParams, - listServiceInterfaces: {} as ListServiceInterfacesParams, - removeAddress: {} as RemoveAddressParams, + getPrimaryUrl: {} as WithCallback, + listServiceInterfaces: {} as WithCallback, exportAction: {} as ExportActionParams, - removeAction: {} as RemoveActionParams, + clearActions: undefined, mount: {} as MountParams, checkDependencies: {} as CheckDependenciesParam, getDependencies: undefined, diff --git a/sdk/lib/test/utils.splitCommand.test.ts b/sdk/lib/test/utils.splitCommand.test.ts deleted file mode 100644 index aafddb177..000000000 --- a/sdk/lib/test/utils.splitCommand.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getHostname } from "../util/getServiceInterface" -import { splitCommand } from "../util/splitCommand" - -describe("splitCommand ", () => { - const inputToExpected = [ - ["cat", ["cat"]], - [["cat"], ["cat"]], - [ - ["cat", "hello all my homies"], - ["cat", "hello all my homies"], - ], - ["cat hello world", ["cat", "hello", "world"]], - ["cat hello 'big world'", ["cat", "hello", "big world"]], - [`cat hello "big world"`, ["cat", "hello", "big world"]], - [ - `cat hello "big world's are the greatest"`, - ["cat", "hello", "big world's are the greatest"], - ], - // Too many spaces - ["cat ", ["cat"]], - [["cat "], ["cat "]], - [ - ["cat ", "hello all my homies "], - ["cat ", "hello all my homies "], - ], - ["cat hello world ", ["cat", "hello", "world"]], - [ - " cat hello 'big world' ", - ["cat", "hello", "big world"], - ], - [ - ` cat hello "big world" `, - ["cat", "hello", "big world"], - ], - ] - - for (const [input, expectValue] of inputToExpected) { - test(`should return ${expectValue} for ${input}`, () => { - expect(splitCommand(input as any)).toEqual(expectValue) - }) - } -}) diff --git a/sdk/lib/trigger/TriggerInput.ts b/sdk/lib/trigger/TriggerInput.ts index 9a52d8ca5..82fe79e07 100644 --- a/sdk/lib/trigger/TriggerInput.ts +++ b/sdk/lib/trigger/TriggerInput.ts @@ -2,5 +2,4 @@ import { HealthStatus } from "../types" export type TriggerInput = { lastResult?: HealthStatus - hadSuccess?: boolean } diff --git a/sdk/lib/trigger/changeOnFirstSuccess.ts b/sdk/lib/trigger/changeOnFirstSuccess.ts index 4c45afe31..3da7284df 100644 --- a/sdk/lib/trigger/changeOnFirstSuccess.ts +++ b/sdk/lib/trigger/changeOnFirstSuccess.ts @@ -5,10 +5,12 @@ export function changeOnFirstSuccess(o: { afterFirstSuccess: Trigger }): Trigger { return async function* (getInput) { - const beforeFirstSuccess = o.beforeFirstSuccess(getInput) - yield let currentValue = getInput() - beforeFirstSuccess.next() + while (!currentValue.lastResult) { + yield + currentValue = getInput() + } + const beforeFirstSuccess = o.beforeFirstSuccess(getInput) for ( let res = await beforeFirstSuccess.next(); currentValue?.lastResult !== "success" && !res.done; diff --git a/sdk/lib/trigger/defaultTrigger.ts b/sdk/lib/trigger/defaultTrigger.ts index bd52dc7cc..69cac2773 100644 --- a/sdk/lib/trigger/defaultTrigger.ts +++ b/sdk/lib/trigger/defaultTrigger.ts @@ -2,7 +2,7 @@ import { cooldownTrigger } from "./cooldownTrigger" import { changeOnFirstSuccess } from "./changeOnFirstSuccess" import { successFailure } from "./successFailure" -export const defaultTrigger = successFailure({ - duringSuccess: cooldownTrigger(0), - duringError: cooldownTrigger(30000), +export const defaultTrigger = changeOnFirstSuccess({ + beforeFirstSuccess: cooldownTrigger(1000), + afterFirstSuccess: cooldownTrigger(30000), }) diff --git a/sdk/lib/trigger/index.ts b/sdk/lib/trigger/index.ts index 6da034262..eb058437f 100644 --- a/sdk/lib/trigger/index.ts +++ b/sdk/lib/trigger/index.ts @@ -1,3 +1,4 @@ +import { ExecSpawnable } from "../util/SubContainer" import { TriggerInput } from "./TriggerInput" export { changeOnFirstSuccess } from "./changeOnFirstSuccess" export { cooldownTrigger } from "./cooldownTrigger" diff --git a/sdk/lib/trigger/lastStatus.ts b/sdk/lib/trigger/lastStatus.ts new file mode 100644 index 000000000..90b8c9851 --- /dev/null +++ b/sdk/lib/trigger/lastStatus.ts @@ -0,0 +1,33 @@ +import { Trigger } from "." +import { HealthStatus } from "../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/lib/trigger/successFailure.ts b/sdk/lib/trigger/successFailure.ts index 1bab27289..7febcd356 100644 --- a/sdk/lib/trigger/successFailure.ts +++ b/sdk/lib/trigger/successFailure.ts @@ -1,32 +1,7 @@ import { Trigger } from "." +import { lastStatus } from "./lastStatus" -export function successFailure(o: { +export const successFailure = (o: { duringSuccess: Trigger duringError: Trigger -}): Trigger { - return async function* (getInput) { - while (true) { - const beforeSuccess = o.duringSuccess(getInput) - yield - let currentValue = getInput() - beforeSuccess.next() - for ( - let res = await beforeSuccess.next(); - currentValue?.lastResult !== "success" && !res.done; - res = await beforeSuccess.next() - ) { - yield - currentValue = getInput() - } - const duringError = o.duringError(getInput) - for ( - let res = await duringError.next(); - currentValue?.lastResult === "success" && !res.done; - res = await duringError.next() - ) { - yield - currentValue = getInput() - } - } - } -} +}) => lastStatus({ success: o.duringSuccess, default: o.duringError }) diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 1f1245adc..4820f419d 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -3,7 +3,7 @@ export * as configTypes from "./config/configTypes" import { DependencyRequirement, SetHealth, - HealthCheckResult, + NamedHealthCheckResult, SetMainStatus, ServiceInterface, Host, @@ -12,6 +12,9 @@ import { LanInfo, BindParams, Manifest, + CheckDependenciesResult, + ActionId, + HostId, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -22,10 +25,12 @@ import { Daemons } from "./mainFn/Daemons" import { StorePath } from "./store/PathBuilder" import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" +import { StringObject, ToKebab } from "./util" export * from "./osBindings" export { SDKManifest } from "./manifest/ManifestTypes" export { HealthReceipt } from "./health/HealthReceipt" +export type PathMaker = (options: { volume: string; path: string }) => string export type ExportedAction = (options: { effects: Effects input?: Record @@ -43,10 +48,14 @@ export namespace ExpectedExports { // /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ // export type dependencies = Dependencies; /** For backing up service data though the startOS UI */ - export type createBackup = (options: { effects: Effects }) => Promise + export type createBackup = (options: { + effects: Effects + pathMaker: PathMaker + }) => 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 + pathMaker: PathMaker }) => Promise // /** Health checks are used to determine if the service is working properly after starting @@ -94,10 +103,7 @@ export namespace ExpectedExports { * Every time a package completes an install, this function is called before the main. * Can be used to do migration like things. */ - export type init = (options: { - effects: Effects - previousVersion: null | string - }) => Promise + export type init = (options: { effects: Effects }) => Promise /** This will be ran during any time a package is uninstalled, for example during a update * this will be called. */ @@ -149,14 +155,6 @@ export type DependencyConfig = { }): Promise } -export type ValidIfNoStupidEscape = A extends - | `${string}'"'"'${string}` - | `${string}\\"${string}` - ? never - : "" extends A & "" - ? never - : A - export type ConfigRes = { /** This should be the previous config, that way during set config we start with the previous */ config?: null | Record @@ -174,7 +172,7 @@ export type Daemon = { [DaemonProof]: never } -export type HealthStatus = HealthCheckResult["result"] +export type HealthStatus = NamedHealthCheckResult["result"] export type SmtpValue = { server: string port: number @@ -183,9 +181,7 @@ export type SmtpValue = { password: string | null | undefined } -export type CommandType = - | ValidIfNoStupidEscape - | [string, ...string[]] +export type CommandType = string | [string, ...string[]] export type DaemonReturned = { wait(): Promise @@ -251,15 +247,15 @@ export type SdkPropertiesValue = } | { type: "string" - /** Value */ + /** The value to display to the user */ value: string /** A human readable description or explanation of the value */ description?: string - /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + /** Whether or not to mask the value, for example, when displaying a password */ masked: boolean - /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + /** Whether or not to include a button for copying the value to clipboard */ copyable?: boolean - /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + /** Whether or not to include a button for displaying the value as a QR code */ qr?: boolean } @@ -275,15 +271,15 @@ export type PropertiesValue = } | { type: "string" - /** Value */ + /** The value to display to the user */ value: string /** A human readable description or explanation of the value */ description: string | null - /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + /** Whether or not to mask the value, for example, when displaying a password */ masked: boolean - /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + /** Whether or not to include a button for copying the value to clipboard */ copyable: boolean | null - /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + /** Whether or not to include a button for displaying the value as a QR code */ qr: boolean | null } @@ -291,52 +287,148 @@ export type PropertiesReturn = { [key: string]: PropertiesValue } +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] + /** Used to reach out from the pure js runtime */ export type Effects = { + // action + + /** Run an action exported by a service */ executeAction(opts: { - serviceId: string | null + packageId?: PackageId + actionId: ActionId input: Input }): Promise + /** Define an action that can be invoked by a user or service */ + exportAction(options: { + id: ActionId + metadata: ActionMetadata + }): Promise + /** Remove all exported actions */ + clearActions(): Promise + + // config + + /** Returns whether or not the package has been configured */ + getConfigured(options: { packageId?: PackageId }): Promise + /** Indicates that this package has been configured. Called during setConfig or init */ + setConfigured(options: { configured: boolean }): Promise - /** A low level api used by makeOverlay */ - createOverlayedImage(options: { imageId: string }): Promise<[string, string]> + // control - /** A low level api used by destroyOverlay + makeOverlay:destroy */ - destroyOverlayedImage(options: { guid: string }): Promise + /** restart this service's main function */ + restart(): Promise + /** stop this service's main function */ + shutdown(): Promise + /** indicate to the host os what runstate the service is in */ + setMainStatus(options: SetMainStatus): Promise - /** Removes all network bindings */ - clearBindings(): Promise + // dependency + + /** Set the dependencies of what the service needs, usually run during the set config 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 }): 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 - /** Retrieves the current hostname(s) associated with a host id */ - // getHostInfo(options: { - // kind: "static" | "single" - // serviceInterfaceId: string - // packageId: string | null - // callback: () => void - // }): Promise + /** Get the port address for a service */ + getServicePortForward(options: { + packageId?: PackageId + hostId: HostId + internalPort: number + }): Promise + /** Removes all network bindings, called in the setupConfig */ + clearBindings(): Promise + // host + /** Returns information about the specified host, if it exists */ getHostInfo(options: { - hostId: string - packageId: string | null - callback: () => void - }): Promise - - // /** - // * Run rsync between two volumes. This is used to backup data between volumes. - // * This is a long running process, and a structure that we can either wait for, or get the progress of. - // */ - // 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 - // } + packageId?: PackageId + hostId: HostId + callback?: () => void + }): Promise + /** Returns the primary url that a user has selected for a host, if it exists */ + getPrimaryUrl(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(): 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 */ @@ -345,7 +437,7 @@ export type Effects = { packageId?: string /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ path: StorePath - callback: (config: unknown, previousConfig: unknown) => void + callback?: () => void }): Promise /** Used to store values that can be accessed and subscribed to */ set(options: { @@ -354,137 +446,15 @@ export type Effects = { 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 - setMainStatus(o: SetMainStatus): Promise - - getSystemSmtp(input: { - callback: (config: unknown, previousConfig: unknown) => void - }): Promise - - /** Get the IP address of the container */ - getContainerIp(): Promise - /** - * Get the port address for another service - */ - getServicePortForward(options: { - internalPort: number - packageId: string | null - }): Promise - - /** Removes all network interfaces */ - clearServiceInterfaces(): Promise - /** When we want to create a link in the front end interfaces, and example is - * exposing a url to view a web service - */ - exportServiceInterface(options: ExportServiceInterfaceParams): Promise - - exposeForDependents(options: { paths: string[] }): Promise - - /** - * There are times that we want to see the addresses that where exported - * @param options.addressId If we want to filter the address id - * - * Note: any auth should be filtered out already - */ - getServiceInterface(options: { - packageId: PackageId | null - serviceInterfaceId: ServiceInterfaceId - callback: () => void - }): Promise - - /** - * The user sets the primary url for a interface - * @param options - */ - getPrimaryUrl(options: GetPrimaryUrlParams): Promise - - /** - * There are times that we want to see the addresses that where exported - * @param options.addressId If we want to filter the address id - * - * Note: any auth should be filtered out already - */ - listServiceInterfaces(options: { - packageId: PackageId | null - callback: () => void - }): Promise> - - /** - *Remove an address that was exported. Used problably during main or during setConfig. - * @param options - */ - removeAddress(options: { id: string }): Promise - - /** - * - * @param options - */ - exportAction(options: { id: string; metadata: ActionMetadata }): Promise - /** - * Remove an action that was exported. Used problably during main or during setConfig. - */ - removeAction(options: { id: string }): Promise - - getConfigured(): Promise - /** - * This called after a valid set config as well as during init. - * @param configured - */ - setConfigured(options: { configured: boolean }): Promise + // system - /** - * - * @returns PEM encoded fullchain (ecdsa) - */ - getSslCertificate: (options: { - packageId: string | null - hostId: string - algorithm: "ecdsa" | "ed25519" | null - }) => Promise<[string, string, string]> - /** - * @returns PEM encoded ssl key (ecdsa) - */ - getSslKey: (options: { - packageId: string | null - hostId: string - algorithm: "ecdsa" | "ed25519" | null - }) => Promise - - setHealth(o: SetHealth): Promise - - /** Set the dependencies of what the service needs, usually ran during the set config 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 - - /** When one wants to checks the status of several services during the checking of dependencies. The result will include things like the status - * of the service and what the current health checks are. - */ - checkDependencies(options: { - packageIds: PackageId[] | null - }): Promise - /** Exists could be useful during the runtime to know if some service exists, option dep */ - exists(options: { packageId: PackageId }): Promise - /** Exists could be useful during the runtime to know if some service is running, option dep */ - running(options: { packageId: PackageId }): Promise - - restart(): Promise - shutdown(): Promise - - mount(options: { - location: string - target: { - packageId: string - volumeId: string - subpath: string | null - readonly: boolean - } - }): Promise - - stopped(options: { packageId: string | null }): Promise + /** Returns globally configured SMTP settings, if they exist */ + getSystemSmtp(options: { callback?: () => void }): Promise } /** rsync options: https://linux.die.net/man/1/rsync @@ -518,12 +488,11 @@ export type MigrationRes = { } export type ActionResult = { + version: "0" message: string - value: null | { - value: string - copyable: boolean - qr: boolean - } + value: string | null + copyable: boolean + qr: boolean } export type SetResult = { dependsOn: DependsOn @@ -549,12 +518,3 @@ export type Dependencies = Array export type DeepPartial = T extends {} ? { [P in keyof T]?: DeepPartial } : T - -export type CheckDependencyResult = { - packageId: PackageId - isInstalled: boolean - isRunning: boolean - healthChecks: SetHealth[] - version: string | null -} -export type CheckResults = CheckDependencyResult[] diff --git a/sdk/lib/util/GetSslCertificate.ts b/sdk/lib/util/GetSslCertificate.ts new file mode 100644 index 000000000..df19607d4 --- /dev/null +++ b/sdk/lib/util/GetSslCertificate.ts @@ -0,0 +1,47 @@ +import { T } from ".." +import { Effects } from "../types" + +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.restart, + }) + } + /** + * 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/lib/util/GetSystemSmtp.ts b/sdk/lib/util/GetSystemSmtp.ts index 1853afd78..498a2d8b2 100644 --- a/sdk/lib/util/GetSystemSmtp.ts +++ b/sdk/lib/util/GetSystemSmtp.ts @@ -15,9 +15,7 @@ export class GetSystemSmtp { * Returns the system SMTP credentials. Does nothing if the credentials change */ once() { - return this.effects.getSystemSmtp({ - callback: () => {}, - }) + return this.effects.getSystemSmtp({}) } /** * Watches the system SMTP credentials. Takes a custom callback function to run whenever the credentials change diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts deleted file mode 100644 index 4d6d36b34..000000000 --- a/sdk/lib/util/Overlay.ts +++ /dev/null @@ -1,238 +0,0 @@ -import * as fs from "fs/promises" -import * as T from "../types" -import * as cp from "child_process" -import { promisify } from "util" -import { Buffer } from "node:buffer" -export const execFile = promisify(cp.execFile) -const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/` -export class Overlay { - private constructor( - readonly effects: T.Effects, - readonly imageId: T.ImageId, - readonly rootfs: string, - readonly guid: T.Guid, - ) {} - static async of( - effects: T.Effects, - image: { id: T.ImageId; sharedRun?: boolean }, - ) { - const { id, sharedRun } = image - const [rootfs, guid] = await effects.createOverlayedImage({ - imageId: id as string, - }) - - const shared = ["dev", "sys", "proc"] - if (!!sharedRun) { - shared.push("run") - } - - 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 Overlay(effects, id, rootfs, guid) - } - - 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 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 - } - - async destroy() { - const imageId = this.imageId - const guid = this.guid - await this.effects.destroyOverlayedImage({ guid }) - } - - async exec( - command: string[], - options?: CommandOptions, - timeoutMs: number | null = 30000, - ): Promise<{ - exitCode: number | null - exitSignal: NodeJS.Signals | null - stdout: string | Buffer - stderr: string | Buffer - }> { - 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", - [ - "chroot", - `--env=/media/startos/images/${this.imageId}.env`, - `--workdir=${workdir}`, - ...extra, - this.rootfs, - ...command, - ], - options || {}, - ) - const pid = child.pid - const stdout = { data: "" as string | Buffer } - const stderr = { data: "" as string | Buffer } - const appendData = - (appendTo: { data: string | Buffer }) => - (chunk: string | Buffer | any) => { - if (typeof appendTo.data === "string" && typeof chunk === "string") { - appendTo.data += chunk - } else if (typeof chunk === "string" || chunk instanceof Buffer) { - appendTo.data = Buffer.concat([ - Buffer.from(appendTo.data), - Buffer.from(chunk), - ]) - } else { - console.error("received unexpected chunk", chunk) - } - } - return new Promise((resolve, reject) => { - child.on("error", reject) - if (timeoutMs !== null && pid) { - setTimeout( - () => execFile("pkill", ["-9", "-s", String(pid)]).catch((_) => {}), - timeoutMs, - ) - } - child.stdout.on("data", appendData(stdout)) - child.stderr.on("data", appendData(stderr)) - child.on("exit", (code, signal) => - resolve({ - exitCode: code, - exitSignal: signal, - stdout: stdout.data, - stderr: stderr.data, - }), - ) - }) - } - - async spawn( - command: string[], - options?: CommandOptions, - ): Promise { - 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", - [ - "chroot", - `--env=/media/startos/images/${this.imageId}.env`, - `--workdir=${workdir}`, - ...extra, - this.rootfs, - ...command, - ], - options, - ) - } -} - -export type CommandOptions = { - env?: { [variable: string]: string } - cwd?: string - user?: string -} - -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 -} diff --git a/sdk/lib/util/SubContainer.ts b/sdk/lib/util/SubContainer.ts new file mode 100644 index 000000000..bbc5c5f64 --- /dev/null +++ b/sdk/lib/util/SubContainer.ts @@ -0,0 +1,435 @@ +import * as fs from "fs/promises" +import * as T from "../types" +import * as cp from "child_process" +import { promisify } from "util" +import { Buffer } from "node:buffer" +import { once } from "./once" +export const execFile = promisify(cp.execFile) +const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/` +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, + ): 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: "ignore", + }) + 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() + }), + ) + } + static async of( + effects: T.Effects, + image: { id: T.ImageId; sharedRun?: boolean }, + ) { + const { id, sharedRun } = image + const [rootfs, guid] = await effects.subcontainer.createFs({ + imageId: id as string, + }) + + 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, id, rootfs, guid) + } + + static async with( + effects: T.Effects, + image: { id: T.ImageId; sharedRun?: boolean }, + mounts: { options: MountOptions; path: string }[], + fn: (subContainer: SubContainer) => Promise, + ): Promise { + const subContainer = await SubContainer.of(effects, image) + 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 { + this.leader.on("exit", () => { + resolve() + }) + if (!this.leader.kill("SIGKILL")) { + 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 }) + } + } + + 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() + } + }), + ) + await new Promise((resolve) => child.stdin.end(resolve)) + } + const pid = child.pid + const stdout = { data: "" as string | Buffer } + const stderr = { data: "" as string | Buffer } + const appendData = + (appendTo: { data: string | Buffer }) => + (chunk: string | Buffer | any) => { + if (typeof appendTo.data === "string" && typeof chunk === "string") { + appendTo.data += chunk + } else if (typeof chunk === "string" || chunk instanceof Buffer) { + appendTo.data = Buffer.concat([ + Buffer.from(appendTo.data), + Buffer.from(chunk), + ]) + } 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, + ): 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, + ): Promise { + return this.subContainer.spawn(command, options) + } +} + +export type CommandOptions = { + env?: { [variable: string]: string } + cwd?: string + user?: string +} + +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/lib/util/asError.ts b/sdk/lib/util/asError.ts new file mode 100644 index 000000000..6e98afb6a --- /dev/null +++ b/sdk/lib/util/asError.ts @@ -0,0 +1,6 @@ +export const asError = (e: unknown) => { + if (e instanceof Error) { + return new Error(e as any) + } + return new Error(`${e}`) +} diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index a2f17be10..fd0fef779 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -50,14 +50,12 @@ export type ServiceInterfaceFilled = { description: string /** Whether or not the interface has a primary URL */ hasPrimary: boolean - /** Whether or not the interface disabled */ - disabled: boolean /** 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 + host: Host | null /** URI information */ - addressInfo: FilledAddressInfo + addressInfo: FilledAddressInfo | null /** Indicates if we are a ui/p2p/api for the kind of interface that this is representing */ type: ServiceInterfaceType /** The primary hostname for the service, as chosen by the user */ @@ -90,6 +88,8 @@ export const addressHostToUrl = ( } 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}]` } else { hostname = host.hostname.value } @@ -183,33 +183,36 @@ const makeInterfaceFilled = async ({ }: { effects: Effects id: string - packageId: string | null - callback: () => void + 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 primaryUrl = await effects - .getPrimaryUrl({ - serviceInterfaceId: id, - packageId, - callback, - }) - .catch((e) => null) + const primaryUrl = await effects.getPrimaryUrl({ + hostId, + packageId, + callback, + }) const interfaceFilled: ServiceInterfaceFilled = { ...serviceInterfaceValue, primaryUrl: primaryUrl, host, - addressInfo: filledAddress(host, serviceInterfaceValue.addressInfo), + addressInfo: host + ? filledAddress(host, serviceInterfaceValue.addressInfo) + : null, get primaryHostname() { if (primaryUrl == null) return null return getHostname(primaryUrl) @@ -221,7 +224,7 @@ const makeInterfaceFilled = async ({ export class GetServiceInterface { constructor( readonly effects: Effects, - readonly opts: { id: string; packageId: string | null }, + readonly opts: { id: string; packageId?: string }, ) {} /** @@ -230,7 +233,7 @@ export class GetServiceInterface { async const() { const { id, packageId } = this.opts const callback = this.effects.restart - const interfaceFilled: ServiceInterfaceFilled = await makeInterfaceFilled({ + const interfaceFilled = await makeInterfaceFilled({ effects: this.effects, id, packageId, @@ -244,12 +247,10 @@ export class GetServiceInterface { */ async once() { const { id, packageId } = this.opts - const callback = () => {} - const interfaceFilled: ServiceInterfaceFilled = await makeInterfaceFilled({ + const interfaceFilled = await makeInterfaceFilled({ effects: this.effects, id, packageId, - callback, }) return interfaceFilled @@ -277,7 +278,7 @@ export class GetServiceInterface { } export function getServiceInterface( effects: Effects, - opts: { id: string; packageId: string | null }, + opts: { id: string; packageId?: string }, ) { return new GetServiceInterface(effects, opts) } diff --git a/sdk/lib/util/getServiceInterfaces.ts b/sdk/lib/util/getServiceInterfaces.ts index c4cdc6b59..9f0e242b8 100644 --- a/sdk/lib/util/getServiceInterfaces.ts +++ b/sdk/lib/util/getServiceInterfaces.ts @@ -11,8 +11,8 @@ const makeManyInterfaceFilled = async ({ callback, }: { effects: Effects - packageId: string | null - callback: () => void + packageId?: string + callback?: () => void }) => { const serviceInterfaceValues = await effects.listServiceInterfaces({ packageId, @@ -27,9 +27,12 @@ const makeManyInterfaceFilled = async ({ hostId, callback, }) + if (!host) { + throw new Error(`host ${hostId} not found!`) + } const primaryUrl = await effects .getPrimaryUrl({ - serviceInterfaceId: serviceInterfaceValue.id, + hostId, packageId, callback, }) @@ -52,7 +55,7 @@ const makeManyInterfaceFilled = async ({ export class GetServiceInterfaces { constructor( readonly effects: Effects, - readonly opts: { packageId: string | null }, + readonly opts: { packageId?: string }, ) {} /** @@ -75,12 +78,10 @@ export class GetServiceInterfaces { */ async once() { const { packageId } = this.opts - const callback = () => {} const interfaceFilled: ServiceInterfaceFilled[] = await makeManyInterfaceFilled({ effects: this.effects, packageId, - callback, }) return interfaceFilled @@ -107,7 +108,7 @@ export class GetServiceInterfaces { } export function getServiceInterfaces( effects: Effects, - opts: { packageId: string | null }, + opts: { packageId?: string }, ) { return new GetServiceInterfaces(effects, opts) } diff --git a/sdk/lib/util/graph.ts b/sdk/lib/util/graph.ts new file mode 100644 index 000000000..5ad71a04d --- /dev/null +++ b/sdk/lib/util/graph.ts @@ -0,0 +1,244 @@ +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, void> { + const veritces = this.vertices + function* gen() { + for (let vertex of veritces) { + if (predicate(vertex)) { + yield vertex + } + } + } + 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, void> { + const visited: Array> = [] + function* rec( + vertex: Vertex, + ): Generator, void> { + if (visited.includes(vertex)) { + return + } + 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 + } + } + } + } + + 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 + } + } + } + })() + } else { + return rec(from) + } + } + reverseBreadthFirstSearch( + to: + | Vertex + | ((vertex: Vertex) => boolean), + ): Generator, void> { + const visited: Array> = [] + function* rec( + vertex: Vertex, + ): Generator, void> { + if (visited.includes(vertex)) { + return + } + 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 + } + } + } + } + + 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 + } + } + } + })() + } else { + return rec(to) + } + } + shortestPath( + from: + | Vertex + | ((vertex: Vertex) => boolean), + to: + | Vertex + | ((vertex: Vertex) => boolean), + ): Array> | void { + const isDone = + to instanceof Function + ? to + : (v: Vertex) => v === to + const path: Array> = [] + const visited: Array> = [] + function* check( + vertex: Vertex, + path: Array>, + ): Generator> | undefined> { + if (isDone(vertex)) { + return path + } + if (visited.includes(vertex)) { + return + } + 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 + } + } + } + } + + 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 + } + } + } + } +} diff --git a/sdk/lib/util/inMs.test.ts b/sdk/lib/util/inMs.test.ts new file mode 100644 index 000000000..fbf71bf2c --- /dev/null +++ b/sdk/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/lib/util/inMs.ts b/sdk/lib/util/inMs.ts new file mode 100644 index 000000000..547fb8bea --- /dev/null +++ b/sdk/lib/util/inMs.ts @@ -0,0 +1,31 @@ +import { DEFAULT_SIGTERM_TIMEOUT } from "../mainFn" + +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/lib/util/index.ts b/sdk/lib/util/index.ts index 63629dfaa..9246cf791 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -3,12 +3,14 @@ import "./fileHelper" import "../store/getStore" import "./deepEqual" import "./deepMerge" -import "./Overlay" +import "./SubContainer" import "./once" export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" +export { asError } from "./asError" export { getServiceInterfaces } from "./getServiceInterfaces" export { addressHostToUrl } from "./getServiceInterface" export { hostnameInfoToAddress } from "./Hostname" export * from "./typeHelpers" export { getDefaultString } from "./getDefaultString" +export { inMs } from "./inMs" diff --git a/sdk/lib/util/splitCommand.ts b/sdk/lib/util/splitCommand.ts index 69f00a5a7..ac1237574 100644 --- a/sdk/lib/util/splitCommand.ts +++ b/sdk/lib/util/splitCommand.ts @@ -1,17 +1,8 @@ import { arrayOf, string } from "ts-matches" -import { ValidIfNoStupidEscape } from "../types" export const splitCommand = ( command: string | [string, ...string[]], ): string[] => { if (arrayOf(string).test(command)) return command - return String(command) - .split('"') - .flatMap((x, i) => - i % 2 !== 0 - ? [x] - : x.split("'").flatMap((x, i) => (i % 2 !== 0 ? [x] : x.split(" "))), - ) - .map((x) => x.trim()) - .filter(Boolean) + return ["sh", "-c", command] } diff --git a/sdk/lib/util/typeHelpers.ts b/sdk/lib/util/typeHelpers.ts index f45a46f1e..d29d5c986 100644 --- a/sdk/lib/util/typeHelpers.ts +++ b/sdk/lib/util/typeHelpers.ts @@ -21,3 +21,96 @@ export type NoAny = NeverPossible extends 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/lib/version/VersionGraph.ts b/sdk/lib/version/VersionGraph.ts new file mode 100644 index 000000000..1b6b49bb9 --- /dev/null +++ b/sdk/lib/version/VersionGraph.ts @@ -0,0 +1,200 @@ +import { ExtendedVersion, VersionRange } from "../exver" + +import * as T from "../types" +import { Graph, Vertex } from "../util/graph" +import { once } from "../util/once" +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), + ) + 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/lib/version/VersionInfo.ts b/sdk/lib/version/VersionInfo.ts new file mode 100644 index 000000000..beea16019 --- /dev/null +++ b/sdk/lib/version/VersionInfo.ts @@ -0,0 +1,78 @@ +import { ValidateExVer } from "../exver" +import * as T from "../types" + +export const IMPOSSIBLE = Symbol("IMPOSSIBLE") + +export type VersionOptions = { + /** The version being described */ + version: Version & ValidateExVer + /** The release notes for this version */ + releaseNotes: string + /** Data migrations for this version */ + migrations: { + /** + * A migration from the previous version + * Leave blank 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[] }, + ) {} + 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/lib/version/index.ts b/sdk/lib/version/index.ts new file mode 100644 index 000000000..c7a47fc38 --- /dev/null +++ b/sdk/lib/version/index.ts @@ -0,0 +1,2 @@ +export * from "./VersionGraph" +export * from "./VersionInfo" diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 0a3655a7d..59f7ab718 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha8", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -22,9 +22,11 @@ "@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" } @@ -1030,6 +1032,41 @@ "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", @@ -1054,6 +1091,42 @@ "@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", @@ -1605,6 +1678,12 @@ "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", @@ -1629,6 +1708,15 @@ "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", @@ -1826,12 +1914,37 @@ "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", @@ -1963,6 +2076,18 @@ "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", @@ -2076,6 +2201,15 @@ "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", @@ -2085,6 +2219,18 @@ "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", @@ -2938,6 +3084,15 @@ "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", @@ -2986,6 +3141,21 @@ "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", @@ -3124,6 +3294,12 @@ "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", @@ -3157,6 +3333,22 @@ "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", @@ -3266,6 +3458,26 @@ } ] }, + "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", @@ -3337,6 +3549,39 @@ "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", @@ -3397,6 +3642,15 @@ "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", @@ -3631,6 +3885,16 @@ "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, + "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", @@ -3674,6 +3938,37 @@ } } }, + "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", diff --git a/sdk/package.json b/sdk/package.json index 8ed984a82..f3af7ff1b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha8", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./cjs/lib/index.js", "types": "./cjs/lib/index.d.ts", @@ -49,9 +49,11 @@ "@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/web/angular.json b/web/angular.json index cf6c113a4..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, diff --git a/web/package-lock.json b/web/package-lock.json index cf32bbc01..ac1f77a6b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "startos-ui", "version": "0.3.6", + "license": "MIT", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", @@ -114,6 +115,7 @@ } }, "../sdk/dist": { + "name": "@start9labs/start-sdk", "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { @@ -130,9 +132,11 @@ "@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/web/package.json b/web/package.json index a75701333..945dde7cf 100644 --- a/web/package.json +++ b/web/package.json @@ -1,8 +1,9 @@ { "name": "startos-ui", - "version": "0.3.6", + "version": "0.3.6-alpha.5", "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", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 7bea3ea14..221b80a3b 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,20 +1,25 @@ { "name": null, - "ack-welcome": "0.3.6", + "ackWelcome": "0.0.0", "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": [] + "widgets": [], + "ack-welcome": "0.3.6-alpha.5" } 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 + + + + + + + + + -
+
-

{{ 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 9a88d0869..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/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 1126f8b54..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,10 +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 a75daa3db..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,10 +1,10 @@ Additional Info - + Other Versions

License

-

{{ manifest.license }}

+

{{ pkg.license }}

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

Instructions

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.wrapperRepo }}

Support Site

-

{{ manifest.supportSite || '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 4c1bfcc65..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.dependencyMetadata[dep.key].title }} + {{ + pkg.dependencyMetadata[dep.key].title + ? pkg.dependencyMetadata[dep.key].title + : dep.key + }} (optional) (Required) 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 a1fed6cca..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,6 +11,7 @@ export class DependenciesComponent { pkg!: MarketplacePkg getImg(key: string): string { - return this.pkg.dependencyMetadata[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 94e7006b1..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,9 +1,11 @@
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

-

Released: {{ pkg.publishedAt | date: 'medium' }}

+

{{ pkg.title }}

+

{{ pkg.version }}

+

+ Released: {{ pkg.s9pk.publishedAt | date : 'medium' }} +

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 502ee82bb..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,7 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, SharedPipesModule, TickerModule, } from '@start9labs/shared' @@ -16,7 +16,7 @@ import { PackageComponent } from './package.component' CommonModule, IonicModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, TickerModule, ], }) diff --git a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts index 48313376f..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,7 +71,8 @@ export class FilterPackagesPipe implements PipeTransform { .filter(p => category === 'all' || p.categories.includes(category)) .sort((a, b) => { return ( - new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf() + new Date(b.s9pk.publishedAt).valueOf() - + new Date(a.s9pk.publishedAt).valueOf() ) }) } diff --git a/web/projects/marketplace/src/public-api.ts b/web/projects/marketplace/src/public-api.ts index b9e8c4686..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,6 +18,8 @@ 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' 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 7b99c7b80..9853cddbb 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -1,48 +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 type StoreIdentityWithData = StoreData & StoreIdentity - -export interface MarketplacePkg { - icon: Url - license: Url - instructions: Url - manifest: T.Manifest - categories: string[] - versions: string[] - dependencyMetadata: { - [id: string]: DependencyMetadata - } - publishedAt: string +export type GetPackagesRes = { + [id: T.PackageId]: GetPackageRes } -export interface DependencyMetadata { - title: string - icon: Url - optional: boolean - hidden: boolean +export type StoreIdentity = { + url: string + name?: string } -export interface Dependency { - description: string | null - optional: boolean +export type Marketplace = Record + +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/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts index dd1dae9ce..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 @@ -4,7 +4,7 @@ import { ErrorService } from '@start9labs/shared' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' import { ApiService, - StartOSDiskInfoWithId, + StartOSDiskInfoFull, } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -16,7 +16,7 @@ import { PasswordPage } from '../../modals/password/password.page' }) export class RecoverPage { loading = true - servers: StartOSDiskInfoWithId[] = [] + servers: StartOSDiskInfoFull[] = [] constructor( private readonly apiService: ApiService, @@ -78,7 +78,7 @@ export class RecoverPage { await modal.present() } - async select(server: StartOSDiskInfoWithId) { + async select(server: StartOSDiskInfoFull) { const modal = await this.modalController.create({ component: PasswordPage, componentProps: { passwordHash: server.passwordHash }, @@ -90,7 +90,7 @@ export class RecoverPage { type: 'backup', target: { type: 'disk', - logicalname: res.data.logicalname, + logicalname: server.partition.logicalname, }, serverId: server.id, password: res.data.password, 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/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/public-api.ts b/web/projects/shared/src/public-api.ts index c83fef860..bb0401ef3 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -30,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' @@ -44,7 +44,7 @@ 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/exver.service' export * from './services/error.service' export * from './services/http.service' 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/exver.service.ts b/web/projects/shared/src/services/exver.service.ts new file mode 100644 index 000000000..f3a28fae2 --- /dev/null +++ b/web/projects/shared/src/services/exver.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core' +import { VersionRange, ExtendedVersion, Version } 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 + } + } + + compareOsVersion(current: string, other: string) { + return Version.parse(current).compare(Version.parse(other)) + } + + satisfies(version: string, range: string): boolean { + return VersionRange.parse(range).satisfiedBy(ExtendedVersion.parse(version)) + } + + getFlavor(version: string): string | null { + return ExtendedVersion.parse(version).flavor + } +} 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/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 00ff64690..101d402b3 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -1,14 +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_ICONS_PATH } from '@taiga-ui/core' +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' @@ -58,6 +60,17 @@ export const APP_PROVIDERS: Provider[] = [ 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( diff --git a/web/projects/ui/src/app/app/menu/menu.component.ts b/web/projects/ui/src/app/app/menu/menu.component.ts index b2ab62368..b956b61ce 100644 --- a/web/projects/ui/src/app/app/menu/menu.component.ts +++ b/web/projects/ui/src/app/app/menu/menu.component.ts @@ -20,7 +20,7 @@ import { AbstractMarketplaceService } from '@start9labs/marketplace' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { SplitPaneTracker } from 'src/app/services/split-pane.service' -import { Emver, THEME } from '@start9labs/shared' +import { Exver, THEME } from '@start9labs/shared' import { ConnectionService } from 'src/app/services/connection.service' import { getManifest } from 'src/app/util/get-package-data' @@ -99,10 +99,10 @@ 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 ( local[id] && - this.emver.compare( + this.exver.compareExver( version, getManifest(local[id]).version || '', ) === 1 @@ -125,7 +125,7 @@ export class MenuComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, - private readonly emver: Emver, + private readonly exver: Exver, private readonly connection$: ConnectionService, ) {} } 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 7e05178bf..837bebf9b 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 { Exver, getErrorMessage } from '@start9labs/shared' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -20,7 +21,7 @@ export class BackupService { constructor( private readonly embassyApi: ApiService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} async getBackupTargets(): Promise { @@ -57,14 +58,15 @@ export class BackupService { hasAnyBackup(target: BackupTarget): boolean { return Object.values(target.startOs).some( - s => this.emver.compare(s.version, '0.3.6') !== -1, + s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less', ) } hasThisBackup(target: BackupTarget, id: string): boolean { return ( target.startOs[id] && - this.emver.compare(target.startOs[id].version, '0.3.6') !== -1 + this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !== + 'less' ) } } 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 index 76c67f837..d66387fb5 100644 --- 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 @@ -28,7 +28,7 @@ [open]="!!open.get(item)" (openChange)="open.set(item, $event)" > - {{ item.value | mustache: $any(spec.spec).displayAs }} + {{ item.value | mustache : $any(spec.spec).displayAs }} 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 index 614aae05c..731d64a63 100644 --- 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 @@ -1,7 +1,6 @@ - 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 index 05ffa69f3..37387a338 100644 --- 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 @@ -18,8 +18,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > @@ -32,8 +32,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit) : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit) : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit) : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit) : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/components/form/form-file/form-file.component.html deleted file mode 100644 index 9e9c16613..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - - -
-
- {{ spec.name }} - * - -
- - - Click or drop file here - -
-
Drop file here
-
-
diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.scss b/web/projects/ui/src/app/components/form/form-file/form-file.component.scss deleted file mode 100644 index 2e314972d..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import '@taiga-ui/core/styles/taiga-ui-local'; - -.template { - @include transition(opacity); - - width: 100%; - display: flex; - align-items: center; - padding: 0 0.5rem; - font: var(--tui-font-text-m); - font-weight: bold; - - &_hidden { - opacity: 0; - } -} - -.drop { - @include fullsize(); - @include transition(opacity); - display: flex; - align-items: center; - justify-content: space-around; - - &_hidden { - opacity: 0; - } -} - -.label { - display: flex; - align-items: center; - max-width: 50%; -} - -small { - max-width: 50%; - font-weight: normal; - color: var(--tui-text-02); - margin-left: auto; -} - -tui-tag { - z-index: 1; - margin: -0.25rem -0.25rem -0.25rem auto; -} diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.ts b/web/projects/ui/src/app/components/form/form-file/form-file.component.ts deleted file mode 100644 index 52d340fa6..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core' -import { TuiFileLike } from '@taiga-ui/kit' -import { CT } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-file', - templateUrl: './form-file.component.html', - styleUrls: ['./form-file.component.scss'], -}) -export class FormFileComponent extends Control {} diff --git a/web/projects/ui/src/app/components/form/form.module.ts b/web/projects/ui/src/app/components/form/form.module.ts index b7a36cd1f..5417309a9 100644 --- a/web/projects/ui/src/app/components/form/form.module.ts +++ b/web/projects/ui/src/app/components/form/form.module.ts @@ -40,7 +40,6 @@ 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 { FormFileComponent } from './form-file/form-file.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' @@ -96,7 +95,6 @@ import { HintPipe } from './hint.pipe' FormNumberComponent, FormSelectComponent, FormMultiselectComponent, - FormFileComponent, FormUnionComponent, FormObjectComponent, FormArrayComponent, 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 50cbc5a56..8cf5ce76d 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,7 +1,7 @@ import { Injectable } from '@angular/core' import { endWith, Observable } from 'rxjs' import { map } from 'rxjs/operators' -import { Emver } from '@start9labs/shared' +import { Exver } 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' @@ -9,13 +9,16 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @Injectable({ providedIn: 'root' }) export class RefreshAlertService extends Observable { private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe( - map(version => !!this.emver.compare(this.config.version, version)), + map( + version => + this.exver.compareOsVersion(this.config.version, version) !== 'equal', + ), endWith(false), ) constructor( private readonly patch: PatchDB, - private readonly emver: Emver, + private readonly exver: Exver, private readonly config: ConfigService, ) { super(subscriber => this.stream$.subscribe(subscriber)) 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 8a607896e..1c2d99f53 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,5 +1,5 @@ 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' @@ -19,7 +19,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( @@ -44,7 +44,9 @@ 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 + // checks to see if backup was made on a newer version of startOS + return ( + this.exver.compareOsVersion(version, this.config.version) === 'greater' + ) } } diff --git a/web/projects/ui/src/app/modals/config.component.ts b/web/projects/ui/src/app/modals/config.component.ts index 194302f5f..d0e8afa4d 100644 --- a/web/projects/ui/src/app/modals/config.component.ts +++ b/web/projects/ui/src/app/modals/config.component.ts @@ -6,7 +6,7 @@ import { isEmptyObject, LoadingService, } from '@start9labs/shared' -import { CT } from '@start9labs/start-sdk' +import { CT, T } from '@start9labs/start-sdk' import { TuiButtonModule } from '@taiga-ui/experimental' import { TuiDialogContext, @@ -203,8 +203,6 @@ export class ConfigModal { const loader = new Subscription() try { - await this.uploadFiles(config, loader) - if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) { await this.configureDeps(config, loader) } else { @@ -217,24 +215,6 @@ export class ConfigModal { } } - private async uploadFiles(config: Record, loader: Subscription) { - loader.unsubscribe() - loader.closed = false - - // TODO: Could be nested files - const keys = Object.keys(config).filter(key => config[key] instanceof File) - const message = `Uploading File${keys.length > 1 ? 's' : ''}...` - - if (!keys.length) return - - loader.add(this.loader.open(message).subscribe()) - - const hashes = await Promise.all( - keys.map(key => this.embassyApi.uploadFile(config[key])), - ) - keys.forEach((key, i) => (config[key] = hashes[i])) - } - private async configureDeps( config: Record, loader: Subscription, @@ -265,11 +245,11 @@ export class ConfigModal { this.context.$implicit.complete() } - private async approveBreakages(breakages: Breakages): Promise { + private async approveBreakages(breakages: T.PackageId[]): Promise { const packages = await getAllPackages(this.patchDb) const message = 'As a result of this change, the following services will no longer work properly and may crash:
    ' - const content = `${message}${Object.keys(breakages).map( + const content = `${message}${breakages.map( id => `
  • ${getManifest(packages[id]).title}
  • `, )}
` const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' } 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 dd4e4b36e..99bd70e48 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 @@ -20,6 +20,20 @@ import { import { getAllPackages, getManifest } from 'src/app/util/get-package-data' import { hasCurrentDeps } from 'src/app/util/has-deps' +const allowedStatuses = { + onlyRunning: new Set(['running']), + onlyStopped: new Set(['stopped']), + any: new Set([ + 'running', + 'stopped', + 'restarting', + 'restoring', + 'stopping', + 'starting', + 'backingUp', + ]), +} + @Component({ selector: 'app-actions', templateUrl: './app-actions.page.html', @@ -46,7 +60,10 @@ export class AppActionsPage { status: T.Status, action: { key: string; value: T.ActionMetadata }, ) { - if (status && action.value.allowedStatuses.includes(status.main.status)) { + if ( + status && + allowedStatuses[action.value.allowedStatuses].has(status.main.status) + ) { if (!isEmptyObject(action.value.input || {})) { this.formDialog.open(FormComponent, { label: action.value.name, @@ -84,7 +101,7 @@ export class AppActionsPage { await alert.present() } } else { - const statuses = [...action.value.allowedStatuses] + const statuses = [...allowedStatuses[action.value.allowedStatuses]] const last = statuses.pop() let statusesStr = statuses.join(', ') let error = '' @@ -150,7 +167,7 @@ export class AppActionsPage { try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) this.embassyApi - .setDbValue(['ack-instructions', this.pkgId], false) + .setDbValue(['ackInstructions', this.pkgId], false) .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html index 9173de39f..8225d7e53 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html @@ -11,7 +11,7 @@

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

{{ manifest.title }}

slot="end" fill="clear" color="primary" - (click)="launchUi($event, pkg.entry.serviceInterfaces)" + (click)="launchUi($event, pkg.entry.serviceInterfaces, pkg.entry.hosts)" [disabled]=" - !(pkg.entry.stateInfo.state | isLaunchable: pkgMainStatus.status) + !(pkg.entry.stateInfo.state | isLaunchable : pkgMainStatus.status) " > 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 35bf99f8f..1464ac8a2 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 @@ -24,14 +24,16 @@ export class AppListPkgComponent { } get sigtermTimeout(): string | null { - return this.pkgMainStatus.status === 'stopping' - ? this.pkgMainStatus.timeout - : null + return this.pkgMainStatus.status === 'stopping' ? '30s' : null // @dr-bonez TODO } - launchUi(e: Event, interfaces: PackageDataEntry['serviceInterfaces']): void { + launchUi( + e: Event, + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { e.stopPropagation() e.preventDefault() - this.launcherService.launch(interfaces) + 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-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 86744cc91..a3c6cc584 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 @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, SharedPipesModule, } from '@start9labs/shared' @@ -49,12 +49,13 @@ const routes: Routes = [ InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), - 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 5b3123131..390c6c642 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,8 +7,7 @@ @@ -34,9 +33,7 @@ - +
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 cb1f6f5f9..52855be3c 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,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { + AllPackageData, DataModel, InstallingState, PackageDataEntry, @@ -47,17 +48,19 @@ export class AppShowPage { private readonly pkgId = getPkgId(this.route) readonly pkgPlus$ = combineLatest([ - this.patch.watch$('packageData', 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] return { pkg, - dependencies: this.getDepInfo(pkg, depErrors), + dependencies: this.getDepInfo(pkg, allPkgs, depErrors), status: renderPkgStatus(pkg, depErrors), } }), @@ -81,17 +84,45 @@ export class AppShowPage { private getDepInfo( pkg: PackageDataEntry, + allPkgs: AllPackageData, depErrors: PkgDependencyErrors, ): DependencyInfo[] { const manifest = getManifest(pkg) return Object.keys(pkg.currentDependencies) .filter(id => !!manifest.dependencies[id]) - .map(id => this.getDepValues(pkg, manifest, id, depErrors)) + .map(id => this.getDepValues(pkg, allPkgs, manifest, id, depErrors)) + } + + private 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 ? title : depId, + icon: icon ? icon : 'assets/img/service-icons/fallback.png', + versionRange, + } + } } private getDepValues( pkg: PackageDataEntry, + allPkgs: AllPackageData, manifest: T.Manifest, depId: string, depErrors: PkgDependencyErrors, @@ -103,11 +134,15 @@ export class AppShowPage { depErrors, ) - const { title, icon, versionSpec } = pkg.currentDependencies[depId] + const { title, icon, versionRange } = this.getDepDetails( + pkg, + allPkgs, + depId, + ) return { id: depId, - version: versionSpec, + version: versionRange, title, icon, errorText: errorText @@ -190,7 +225,7 @@ export class AppShowPage { const dependentInfo: DependentInfo = { id: pkgManifest.id, title: pkgManifest.title, - version: pkg.currentDependencies[depId].versionSpec, + version: pkg.currentDependencies[depId].versionRange, } const navigationExtras: NavigationExtras = { state: { dependentInfo }, 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 73fc2158b..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 @@ -6,11 +6,11 @@

Version

-

{{ manifest.version | displayEmver }}

+

{{ pkg.stateInfo.manifest.version }}

Git Hash

License

-

{{ manifest.license }}

+

{{ pkg.stateInfo.manifest.license }}

Marketing Site

-

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

+

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

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

Marketing Site

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.stateInfo.manifest.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.stateInfo.manifest.wrapperRepo }}

Support Site

-

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

+

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

Donation Link

-

{{ manifest.donationUrl || '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 278fa3d88..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 @@ -1,9 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ModalController, ToastController } from '@ionic/angular' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' +import { + InstalledState, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-additional', @@ -12,7 +15,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' }) export class AppShowAdditionalComponent { @Input() - manifest!: T.Manifest + pkg!: PackageDataEntry constructor( private readonly modalCtrl: ModalController, @@ -35,16 +38,12 @@ export class AppShowAdditionalComponent { } async presentModalLicense() { - const { id, version } = this.manifest + const { id } = this.pkg.stateInfo.manifest const modal = await this.modalCtrl.create({ componentProps: { title: 'License', - content: from( - this.api.getStatic( - `/public/package-data/${id}/${version}/LICENSE.md`, - ), - ), + 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..b184b83ff 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 @@ -15,7 +15,7 @@

> {{ dep.title }}

-

{{ dep.version | displayEmver }}

+

{{ dep.version }}

{{ dep.errorText || 'satisfied' }} 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 06f517222..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 @@ -9,7 +9,7 @@

{{ manifest.title }}

-

{{ 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.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index fef84a5ba..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 @@ -10,15 +10,15 @@ import { ConnectionService } from 'src/app/services/connection.service' }) export class AppShowHealthChecksComponent { @Input() - healthChecks!: Record + healthChecks!: Record constructor(readonly connection$: ConnectionService) {} - isLoading(result: T.HealthCheckResult['result']): boolean { + isLoading(result: T.NamedHealthCheckResult['result']): boolean { return result === 'starting' || result === 'loading' } - isReady(result: T.HealthCheckResult['result']): boolean { + isReady(result: T.NamedHealthCheckResult['result']): boolean { return result !== 'failure' && result !== 'loading' } 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 9b62a6402..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 @@ -2,17 +2,19 @@

{{ phase.name }} - : {{ progress * 100 }}% + : {{ 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 b6d259553..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 @@ -9,5 +9,5 @@ import { T } from '@start9labs/start-sdk' }) export class AppShowProgressComponent { @Input() - phases!: T.FullProgress['phases'] + 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 be7bc1c30..c5d18907c 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 @@ -56,13 +56,13 @@ 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 30657ae01..308db67be 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 @@ -55,6 +55,10 @@ export class AppShowStatusComponent { return this.pkg.serviceInterfaces } + get hosts(): PackageDataEntry['hosts'] { + return this.pkg.hosts + } + get pkgStatus(): T.Status { return this.pkg.status } @@ -76,13 +80,14 @@ export class AppShowStatusComponent { } get sigtermTimeout(): string | null { - return this.pkgStatus?.main.status === 'stopping' - ? this.pkgStatus.main.timeout - : null + return this.pkgStatus?.main.status === 'stopping' ? '30s' : null // @dr-bonez TODO } - launchUi(interfaces: PackageDataEntry['serviceInterfaces']): void { - this.launcherService.launch(interfaces) + launchUi( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { + this.launcherService.launch(interfaces, hosts) } async presentModalConfig(): Promise { 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 30d71d427..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 @@ -5,7 +5,7 @@ import { T } from '@start9labs/start-sdk' name: 'healthColor', }) export class HealthColorPipe implements PipeTransform { - transform(val: T.HealthCheckResult['result']): string { + transform(val: T.NamedHealthCheckResult['result']): string { switch (val) { case 'success': return 'success' 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 e2a03ecf8..87bb5423f 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 @@ -10,7 +10,6 @@ import { import { ApiService } from 'src/app/services/api/embassy-api.service' import { from, map, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' -import { T } from '@start9labs/start-sdk' import { FormDialogService } from 'src/app/services/form-dialog.service' import { ConfigModal, PackageConfigData } from 'src/app/modals/config.component' @@ -32,6 +31,7 @@ export class ToButtonsPipe implements PipeTransform { private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, private readonly apiService: ApiService, + private readonly api: ApiService, private readonly patch: PatchDB, private readonly formDialog: FormDialogService, ) {} @@ -42,7 +42,7 @@ export class ToButtonsPipe implements PipeTransform { return [ // instructions { - action: () => this.presentModalInstructions(manifest), + action: () => this.presentModalInstructions(pkg), title: 'Instructions', description: `Understand how to use ${manifest.title}`, icon: 'list-outline', @@ -103,17 +103,20 @@ export class ToButtonsPipe implements PipeTransform { ] } - private async presentModalInstructions(manifest: T.Manifest) { + private async presentModalInstructions( + pkg: PackageDataEntry, + ) { this.apiService - .setDbValue(['ack-instructions', 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( - `/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`, + this.api.getStaticInstalled( + pkg.stateInfo.manifest.id, + 'instructions.md', ), ), }, 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 f66773f2c..24153caf9 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 @@ -14,7 +14,7 @@ export class ToHealthChecksPipe implements PipeTransform { transform( manifest: T.Manifest, - ): Observable | null> { + ): Observable | null> { return this.patch.watch$('packageData', manifest.id, 'status', 'main').pipe( map(main => { return main.status === 'running' && !isEmptyObject(main.health) diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index 245e1ae24..c58e7777e 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -29,9 +29,7 @@ export class InitService extends Observable { from(this.api.initGetProgress()), ).pipe( switchMap(({ guid, progress }) => - this.api - .openWebsocket$(guid, {}) - .pipe(startWith(progress)), + this.api.openWebsocket$(guid).pipe(startWith(progress)), ), map(({ phases, overall }) => { return { @@ -58,9 +56,7 @@ export class InitService extends Observable { } }), catchError(e => { - // @TODO this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner - this.errorService.handleError(e) - + console.error(e) return EMPTY }), ) 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 index edce1f282..de05d33bd 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.component.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.component.ts @@ -24,10 +24,10 @@ export class LogsComponent { scroll = true scrollTo(bottom: HTMLElement) { - if (this.scroll) bottom.scrollIntoView({ behavior: 'smooth' }) + if (this.scroll) bottom.scrollIntoView() } - onBottom([{ isIntersecting }]: readonly IntersectionObserverEntry[]) { - this.scroll = isIntersecting + onBottom(entries: readonly IntersectionObserverEntry[]) { + this.scroll = entries[entries.length - 1].isIntersecting } } 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 index 5fe550d45..7ff3cecd1 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.service.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -38,8 +38,8 @@ export class LogsService extends Observable { private readonly log$ = defer(() => this.api.initFollowLogs({ boot: 0 }), ).pipe( - switchMap(({ guid }) => this.api.openWebsocket$(guid, {})), - bufferTime(250), + switchMap(({ guid }) => this.api.openWebsocket$(guid)), + bufferTime(500), filter(logs => !!logs.length), map(convertAnsi), scan((logs: readonly string[], log) => [...logs, log], []), 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.ts b/web/projects/ui/src/app/pages/login/login.page.ts index 065a4cc7f..29d4321f8 100644 --- a/web/projects/ui/src/app/pages/login/login.page.ts +++ b/web/projects/ui/src/app/pages/login/login.page.ts @@ -40,6 +40,7 @@ export class LoginPage { await this.api.login({ password: this.password, metadata: { platforms: getPlatforms() }, + ephemeral: window.location.host === 'localhost', }) this.password = '' 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 c10ce42aa..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 { @@ -34,7 +34,7 @@ const routes: Routes = [ FormsModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, FilterPackagesPipeModule, MarketplaceStatusModule, BadgeMenuComponentModule, 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 e1f13883a..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,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' 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' @@ -20,12 +21,21 @@ 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 } }), ) @@ -78,7 +88,7 @@ export class MarketplaceListPage { private readonly route: ActivatedRoute, ) {} - category = 'featured' + category = 'all' query = '' async presentModalMarketplaceSettings() { 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 6995dde5e..797a2fb13 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,11 +1,15 @@
- View Installed + {{ + localPkg.stateInfo.state === 'installed' + ? 'View Installed' + : 'View Installing' + }} - - 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 5c83c536f..0b76579bc 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 @@ -10,7 +10,7 @@ import { MarketplacePkg, } from '@start9labs/marketplace' import { - Emver, + Exver, ErrorService, isEmptyObject, LoadingService, @@ -44,6 +44,9 @@ export class MarketplaceShowControlsComponent { @Input() localPkg!: PackageDataEntry | null + @Input() + localFlavor!: boolean + readonly showDevTools$ = this.ClientStorageService.showDevTools$ constructor( @@ -52,7 +55,7 @@ export class MarketplaceShowControlsComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly loader: LoadingService, - private readonly emver: Emver, + private readonly exver: Exver, private readonly errorService: ErrorService, private readonly patch: PatchDB, ) {} @@ -79,7 +82,7 @@ export class MarketplaceShowControlsComponent { const localManifest = getManifest(this.localPkg) if ( - this.emver.compare(localManifest.version, this.pkg.manifest.version) !== + this.exver.compareExver(localManifest.version, this.pkg.version) !== 0 && hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) ) { @@ -136,9 +139,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)) { @@ -152,7 +155,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) @@ -179,7 +182,7 @@ export class MarketplaceShowControlsComponent { private async install(url: string) { 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) 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 058cbb158..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,6 +13,7 @@ import { AdditionalModule, DependenciesModule, PackageModule, + FlavorsModule, } from '@start9labs/marketplace' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceShowPage } from './marketplace-show.page' @@ -35,13 +36,14 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, MarketplaceStatusModule, PackageModule, AboutModule, DependenciesModule, AdditionalModule, + FlavorsModule, UiPipeModule, ], declarations: [ 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..a0f7622ad 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,21 +1,14 @@ - + -
+
-

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

+

{{ pkgId }} not found in this registry

@@ -25,21 +18,26 @@

[url]="url" [pkg]="pkg" [localPkg]="localPkg$ | async" + [localFlavor]="!!(localFlavor$ | async)" > + 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 876e71924..018ddbb64 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,15 @@ 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 { 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, map, 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' @Component({ selector: 'marketplace-show', @@ -17,21 +21,61 @@ 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(([pkg, paramMap]) => + this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor') + ? pkg + : null, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ) - readonly localPkg$ = this.patch - .watch$('packageData', 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 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'), + ), + ), + ), ), ) 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 8958cdda2..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,13 +1,13 @@ - +
Installed Update Available 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 9db50dc29..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 @@ -8,6 +8,7 @@ import { isRestoring, getManifest, } from 'src/app/util/get-package-data' +import { Exver } from '@start9labs/shared' @Component({ selector: 'marketplace-status', @@ -16,8 +17,7 @@ import { }) export class MarketplaceStatusComponent { @Input() version!: string - - @Input() localPkg?: PackageDataEntry + @Input() localPkg!: PackageDataEntry isInstalled = isInstalled isInstalling = isInstalling @@ -26,6 +26,15 @@ export class MarketplaceStatusComponent { isRestoring = isRestoring get localVersion(): string { - return this.localPkg ? getManifest(this.localPkg).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 c31f4fd16..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,7 +1,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' import { MarketplaceStatusComponent } from './marketplace-status.component' @@ -9,7 +9,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component' imports: [ CommonModule, IonicModule, - EmverPipesModule, + ExverPipesModule, InstallingProgressPipeModule, ], declarations: [MarketplaceStatusComponent], 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/server-routing.module.ts b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts index e7eb43aad..945f1b1c9 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 @@ -76,7 +76,7 @@ const routes: Routes = [ import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule), }, { - path: 'wifi', + path: 'wireless', loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule), }, 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 01ecf897e..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 @@ -50,8 +50,7 @@

Clock sync failure

- this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['wireless'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, 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..503aa57ad 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,7 +3,7 @@ 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' @@ -20,7 +20,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), QRComponentModule, - EmverPipesModule, + ExverPipesModule, TuiLetModule, ], 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 f9df6eb37..03d7ef3d7 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 }}

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 3e349b49b..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,8 +52,15 @@ >

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

-

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

-

{{ currentSession.userAgent }}

+

{{ agent }}

+

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

+

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

@@ -78,8 +85,15 @@

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

>

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

-

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

-

{{ session.userAgent }}

+

{{ agent }}

+

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

+

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

{ - return { - id, - ...session, - } - }) - .sort((a, b) => { - return ( - new Date(b.lastActive).valueOf() - new Date(a.lastActive).valueOf() - ) - }) + .map(([id, session]) => ({ id, ...session })) + .sort( + (a, b) => + new Date(b.lastActive).valueOf() - new Date(a.lastActive).valueOf(), + ) } catch (e: any) { this.errorService.handleError(e) } finally { @@ -108,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 a0da8609d..b5f684103 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,10 +1,12 @@ import { Component } from '@angular/core' -import { isPlatform, NavController } from '@ionic/angular' +import { isPlatform } from '@ionic/angular' import { ErrorService, LoadingService } from '@start9labs/shared' -import { S9pk, T } from '@start9labs/start-sdk' +import { S9pk } from '@start9labs/start-sdk' import cbor from 'cbor' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' +import { SideloadService } from './sideload.service' +import { firstValueFrom } from 'rxjs' interface Positions { [key: string]: [bigint, bigint] // [position, length] @@ -22,7 +24,7 @@ const VERSION_2 = new Uint8Array([2]) export class SideloadPage { isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android') toUpload: { - manifest: T.Manifest | null + manifest: { title: string; version: string } | null icon: string | null file: File | null } = { @@ -36,12 +38,14 @@ export class SideloadPage { message: string } + readonly progress$ = this.sideloadService.progress$ + constructor( private readonly loader: LoadingService, private readonly api: ApiService, - private readonly navCtrl: NavController, private readonly errorService: ErrorService, private readonly config: ConfigService, + private readonly sideloadService: SideloadService, ) {} handleFileDrop(e: any) { @@ -111,15 +115,15 @@ export class SideloadPage { } async handleUpload() { - const loader = this.loader.open('Uploading package').subscribe() + const loader = this.loader.open('Starting upload').subscribe() try { const res = await this.api.sideloadPackage() + this.sideloadService.followProgress(res.progress) this.api .uploadPackage(res.upload, this.toUpload.file!) .catch(e => console.error(e)) - - this.navCtrl.navigateRoot('/services') + await firstValueFrom(this.sideloadService.websocketConnected$) } catch (e: any) { this.errorService.handleError(e) } finally { @@ -138,8 +142,8 @@ 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) { @@ -148,7 +152,7 @@ export class SideloadPage { 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]), @@ -158,12 +162,11 @@ export class SideloadPage { this.toUpload.manifest = await cbor.decode(data, true) } - async getIcon(positions: Positions, file: Blob) { - const contentType = '' // @TODO + 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) } 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..79f871bba --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { endWith, ReplaySubject, shareReplay, Subject, switchMap } from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +@Injectable({ + providedIn: 'root', +}) +export class SideloadService { + private readonly guid$ = new Subject() + + readonly websocketConnected$ = new ReplaySubject() + + readonly progress$ = this.guid$.pipe( + switchMap(guid => + this.api + .openWebsocket$(guid, { + openObserver: { + next: () => this.websocketConnected$.next(''), + }, + }) + .pipe(endWith(null)), + ), + 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.ts b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index 5e474db0f..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 @@ -80,7 +80,7 @@ const ADD_OPTIONS: Partial> = { label: 'SSH Key', data: { message: - 'Enter the SSH public key you would like to authorize for root access to your Embassy.', + 'Enter the SSH public key you would like to authorize for root access to your StartOS Server.', }, } 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 08387fcbc..1ddb44225 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 @@ -3,8 +3,8 @@ - WiFi Settings - + Wireless Settings + Refresh @@ -13,163 +13,149 @@ - - - - - - -

WiFi is deprecated

-

- WiFi will be eliminated from StartOS in version v0.4.0, expected soon. - Before then, we highly recommend switching your server to a direct, - Ethernet connection, which is faster and more reliable. If using - Ethernet is not possible for you, we recommend using a WiFi extender - instead. -

-
- - Learn More - - -
- - 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 0a1f6a7d9..5d722b482 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 @@ -9,11 +9,14 @@ import { WINDOW } from '@ng-web-apis/common' import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' import { CT } 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 { 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 @@ -31,6 +34,7 @@ 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, @@ -41,6 +45,8 @@ export class WifiPage { 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, ) {} 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 7b6c6594c..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,7 +5,7 @@ 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 { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, } from '@start9labs/shared' @@ -34,7 +34,7 @@ const routes: Routes = [ RoundProgressModule, InstallingProgressPipeModule, StoreIconComponentModule, - EmverPipesModule, + 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 2bb3860c6..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,27 +30,21 @@ >
- + -

{{ pkg.manifest.title }}

+

{{ pkg.title }}

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

-

+

{{ error }}

@@ -59,7 +53,7 @@

*ngIf="local.stateInfo.state === 'updating' else notUpdating" > - {{ marketplaceService.updateErrors[pkg.manifest.id] ? - 'Retry' : 'Update' }} + {{ marketplaceService.updateErrors[pkg.id] ? 'Retry' : + 'Update' }} @@ -88,12 +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 d1ffc4829..e25632bc8 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.ts +++ b/web/projects/ui/src/app/pages/updates/updates.page.ts @@ -13,7 +13,7 @@ import { 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, map, Observable } from 'rxjs' import { AlertController, NavController } from '@ionic/angular' @@ -24,7 +24,6 @@ import { isUpdating, } from 'src/app/util/get-package-data' import { dryUpdate } from 'src/app/util/dry-update' -import { T } from '@start9labs/start-sdk' interface UpdatesData { hosts: StoreIdentity[] @@ -59,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) { @@ -70,29 +69,29 @@ export class UpdatesPage { }) } - async tryUpdate(manifest: T.Manifest, url: string, 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 - // manifest.id OK because same as local id for update - if (hasCurrentDeps(manifest.id, await getAllPackages(this.patch))) { - 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: T.Manifest, 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)) { @@ -159,18 +158,19 @@ 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>, ): MarketplacePkg[] { - return pkgs.filter(({ manifest }) => { - const localPkg = local[manifest.id] + return pkgs.filter(({ id, version, flavor }) => { + const localPkg = local[id] return ( localPkg && - this.emver.compare( - manifest.version, + 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/pipes/install-progress/install-progress.pipe.ts b/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts index 157afed4e..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 @@ -19,9 +19,9 @@ export class InstallingProgressDisplayPipe implements PipeTransform { name: 'installingProgress', }) export class InstallingProgressPipe implements PipeTransform { - transform(progress: T.Progress): number | null { - if (progress === true) return 1 - if (progress === false || progress === null || !progress.total) return null - return Number((progress.done / progress.total).toFixed(2)) + 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/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,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAABAKADAAQAAAABAAABAAAAAACU0HdKAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAABAAElEQVR4AezdC7xt13go8HXOSaKty9UrFan22qkmFKFRhOu1nZAQLUmEkAgh8SglqkokIYeQeKuoIKk8RNKESoIG8TwR7yqtoIhoUCWE9tbVNpKc3O8/9vpW5tnZa86x9uusvfcav99ac605x/Mb33t8Y8x1vUkaJwisi86sm56eXv/zn/983S1ucYsbNm/efN2wDt7tbne7+fbbb3/r9evX73TDDTfsvGXLlp3WrVu3U+S/TXx2jHu3iv+3iN+3jI/rzePzK3H/ZnF/u/i9IT7Slrh3bdz7Zfz+r/j8Ij4/94n7/xH3/y1+/zQ+P47/V8X1Rxs2bPhRtHfVtdde+9OvfOUr8s+ZYizbNcayJTLd0P/MmX9yc3khAOEmaRtC4DGPecyG73znO+vbiD0I/TZBcHcIQt8tiHG3IMJd4zMVv3eOrt86rr8az3px3Wokkaf8H3bdKnP8yfLDrplffUH8vbhiFj+N6w+jzJXx+9tx/Ub8/+Z222135ec//3nM4iYpmcLv/M7vbHn3u999/U0yTG4sGwS2xphla3bNNrSVhP/7v//7a2dD4g/+4A9I8jsHId0jnt0jrneJ6+3jesskcgQoueYnnl8fv+eSrqXNyJ5zPfta6lJd/0fzOmd90da6aGpDXArTcJVcdaHPHGgQV8b/r8X1S8HAvhTXr/3d3/3dj+K6VYoxb99ngBMNYSvILP2fRIalb2kNt5BSPgietIPkg3Sve93rd4JI9ozP/eNzryCiOwWh3wKxS31iQljKJYHkvLnmR/a87/dSpmFMwn19wBykXnMcMRYmxTfi/t/F59L4fCEYwndmdXR9MIQNE+1gFlSW6O9yIcwSdX+sqy2IPJvo99hjj98IxL9PfB4SvX9QfO4c0nH7+N+Ungg9VWOcwGelzRVmkEwrfhamEPxgxlQJRtC7/vrraUBfj88l8f8jYTZ89gtf+AJfQ6Y5YZgPJ9eFQ2ClIdXCR7yENTQkPcddSsneve99791C+u0dSP7wuH/fIPhfR/Cku0+kzG8+kthX29yARzIFV+PbDkPw6TMEzsbPxu8PBDP4SDCDb8X/TOtCM9huohkkOBbnutqQbHGgMlotvPbFm9702CP6kHD7BzI/Moh9z0DoDZAcwcc1TYGVKt1Hg9Dw3MkQcMH1AacNqSFcd911fBqfj//vjc+FTWYQ8LaC0Qt4g6M6JmmeEJgwgHkCriHtB468u9/97reLZbn9AnEPimrvF0S/HtEHI9BKkfKB5O7l8ts8W1+dxQI2iB4zKNpBaErFsRjMYEs8+1Qwz3cFLC/8x3/8xx8kBDgQJ1pBQmP064QBjAizhvQp6/ObNm1a//73v//hgbhPDiR9WBD9zRtEjzmsdSk/IoQH2ZvawfYNZvCfAd8PBqxPj5WDi1Prmj0vg1omP1ohMGEAreAZPCxqfiDbwKkVzrzbh2r6xMhxaCDnrnL21fu056mp2xS+GNFipCC2xahmIXUYCLhGV9YVv4HKQhu4Ii7vCLif+eUvf/m77kVaH8xg/cQ8KLDo/NrmM9vZw22boTiemuv197znPe8XhPWs6NYBIe1vhujjU7SBuJfSftl6PZvIESs7uvlxL+/XdKzPyAarEv0xJoPbqgr1LnMaMOEYY2EGYSJcE304P/ry5i9+8Yufzv4wD5pzl/cn1xshsOyzd2PTY/3rJoR/j3vc44BAuCMDyR4I6ftEkir+ktr0SeRNos7f+uIjD19DhOb2/vu//7v3i1/8ovd//+//7f30p81VtdFhfpvb3KYXqnbv5je/ee9XfuVXeuHj6FHHJW36JIPQfjIPz/VriVNxpkY724NHvy+XxvUvvvSlL52fbfcZQWpmeXtyDQgs+QytNCg3pQZH3xVXXHFoINifBtLfzVgCyUkgiIcKSPxFT0lYkBrB+fiNuBB3xNb3fvCDH5Tr7MZ322233m//9m/3dtxxx96tbnWr3q//+q8Xwv21X/u13v/4H/9jUJf6mgnxaveXv/xl7//9v//X+8///M/SFgaCkVx11VW973//+71//ud/bhYrv29961v3dtppp8Iodthhh0Ff1RXSufQbM1hChjCYk5inMrAYz2UxntcHIzgjO9yc27y31q8TBtDHgOlYWkq7kWPvfe973+GBsC8INf93+xIO0bNFyxLUYiEOopMQJMnqE22We4j9Rz/6Ue973/te+e8LUe+555698HwXQr/tbW9biB2h3/KWt+whdJL6Zje7WakL80jiaxL9bGLMfmgjpbgrjQJzuOaaawaaxX/8x3/0/u3f/q33k5/8ZNC/b3/7271PfOITig+SPtIg9EVdmIG6+vAs+Wb3Y1B4/j+KryDGWkKVo71vRhuvDtPgtH6V6c9Js23+La2CkmueAfQJf2BXhqp/SCDMi4MI79hHVGo+OJH4iwIvxOaD2BEHgkccCOuyyy4boNXU1FTv//yf/9O7853vXAj+dre7XSH2JHTSVllElHW66nfzv99SXgcNzPGjSZB+Nz8YSPO/dpI50BhoCj/+8Y+LdhIbnHpf+9rXeh/72Md6P/vZz0pLmBMNhSZi7JgLxqKeJnOao1uj3jLgMqdRb4myjH4KQT4hGMFZ/crSWbimGcGiIPSoszMm+TPMtKzjh3PPUt4rAjH3QCiBlItu3yMeiE4qI1xEE2vaRe0GE8R+3/vet3eXu9yld4c73KG38847F4n/q7/6qwOtALE0P7OJWhuZmr/z3nyuzTaav9WVTAFB52/3MTTjQ/z/+q//2qMhfPWrX+19+tOf7oVaLkuP6bDrrrsWJogZpIZQHi7eV/ETNBjBlwMuxwYj+IAm+mZBybN4Ta6cmm7ElpXT54X2dCsHX2zG+f0gqFcGAu+z2ISvPh+SmqT3+9///d8HUn6XXXbp7bvvvr1gPr073vGOvd/8zd8sBC+vhNARhauymZKw85r3t9U1+5ZX/dA3TCEZg2dMGqYD/8U//dM/9WIjUO+cc84pjEKZ3//93y9+BL9pBpgCprJIqTgBkxEETD8UWsFRsXz4j+pfq47CNcUA+up+seUD2W4VyPmKQNRnQrJAhkW18RFAqvccaaSf9KAHPaj3kIc8BML1fvd3f7eo9Gx2+XUhCV5e93xWakL0TaaQDMHVfRoCk+Hyyy8vzOCDH/xgL5btynAjzqL3P//n/yzaEWdis54FwqP4CKIPG/qM9eSA+zH/8A//8O9R75rzD6xc7BoNC7aS+kF8R0TxE0MN3zEIjmiFFAsK3IGgTaKH3KFmll4G4ymSPrSNotpz2DEDEDwpBxGlJPa8lpur6AuMkpCNEeNlCrmCw9VXX11MhThIpHfhhRcOmAGnJyaJEdAMpAXCaDDn0f66wIGro8oXBfP5K3WvJW1g1TOAID7efQRO1b5rIODJMekPQHzx2xFY28dnwXBgp0PKb3zjG8U7zrY9+OCDew94wAOKeo/oSb4keoQg/wIR2bBWdErml8wAjGhBmMHXv/71Xsxd75RTTikOxqmpqd7tb3/78pw5sQgJI3AU2g79dp1R8Mxg3EVda+LOIrQ1llUsGPHHclQznVoXzrTtwxONyHH1l8blJX0iXLCDDwGz7X0gY+xWK60++clPLtKePWuJjoSD0D4Toi8gGvqVzABTpCGZK1Kfz4A2FUuzvb/5m78p5TlL5UlfwQIZaXECRnvbY9BR10ujvU0aChzaIXAIvtzohPFglaRVyQCanDu25d4zJvW0IMTdEWEkk0nqzyshYkRtSYv9Skr91m/9Vu+P//iPexs3bixebVFzkDltV8i5QASdV1+7ChlLpnHrn7750AwwWf2zzEjD+vCHP9w74YQTStcxWr4CJlefeHNI87kW3Ogz7ctiDp8STsJixzVxaj4Vj2uZVccA+hy7SP1Q+TcF4I+DPDGZC1L3ISOnHmT84Q9/2PvWt75VCP4JT3hC7373u1/PGn1KLIgoQd6lTk0ibv7OdpOw8+p+/s5rlsurPPk7r+5lynL+N3/n88W+pmaAMEl9/oIrr7yy98lPfrL3V3/1V71w4PV233333v/6X/+raAQY7wL6VcyCmLsd+mPfFNoA7TG1gYJbiz3GbVXfamIAg8COUPfvEAA9OxBmz5D6JhRFziuCD/Ihek4owS3CYffbb7/eoYce6qSf4sWXZzmkfRJjXiG5D0aTnybiy+ejfz7N3/k/62jWk78xNKlZZ5Zr1tnsz+z8pYJF+srxGKs5kURKii047bTTSiSi5VTxE/ZCLFAjKI7hvjbw+WjqkHASXtHXBAaBY6UTK/hrVTCA/qQU/T4i+Q4LhD2lb8/NW+pDNtIG4QtksVR10EEHFcK3bk/tZFKQRhKkXMyURJVXRIggfbItRKgPfBD/9V//VWL4xfFTlV2tuVONRRj6j0mxmd3Lvqs3JStHpvEiLhuAchOQ8GO/RSDmPSZQLnPqj34iOJ9kEuCRzCOviwUjbagzfQWWWj/72c/23v72t/c+9KEP9eIo9bIPAlyMdZ7tF20gYL5DjOvaGOPTcm9BE+cWa0zbop4VzwBS5e+f0PP2QOYnQcKYrHnZ+hAZsghXzfX7/fffv3f44Yf3LOPl2nQi1TwRa8651raPlMTuKmkPMiNuHnKSj3PsX/7lX8peASqxqEKSry3xTyBkRN+sW/3q1kZbsslIaPIuEcRk0xH/hwAmDk+RferGSNRtLE2moF7wWgqYYQTGJPLwM5/5TO9Nb3pTMRFCIJS5xAAX0K6VgnIoScDpzNjjcLj3GSTutcFr3J+tZAYwWNsPiXzHQLYLAgF+LyaIJkAcz0skk2wkpbXoBz7wgb3nPOc5vfvf//4lQs991S8WEiexu6oTAvtIpDqCROjf/e53iwbC4Sh6jjbSTIjQMiPCS4nYRPaUyNppfpp15Jia19Q05FMOMYMBDQJz1LdmYocL4MEgxPz/7//9v8suQUwhVXbw88kxK9/sa7O+UX7nuJIRYJIf//jHe6961auKo5afRlrA8mFR+2N+tov+/1P0ef/wDXxzpccMrFQGkMS9JSbgMTGvZweybh+ILkpkJo7WbFcmBEL1hTwcS6T/X/zFX/T23nvv3m/8xm8UNX8xCV97EimJ4BGa+oUJk+jf/OY3e/G6rcKE2LeZpqamSn/0FdFAenXlJ/8nMWS5ua6ziU6ZtiS/j77mJ/+76gPiym3DWddee+1VNCcqudgI2gItytgxFONOeKh3oSnHnoyAlhRHtvX+9E//tFQtElM/MbLUUkZs85rop4NgCJqDwy/w7rhmx2cmdsQKt2X2FccAmrZXSP5XxGQcDYFi4kdW+SELpEPwVGifTZs2FVtfwIl6IUoi/0ImKhFTe5DTFSJaSrSiIAT2kksu2WpL7V3veteieWASKX0RjLok/RqH1OyPsfkgLrDjhxD3n+nhD394CY4Kxl2iIjFY2gFYZ1TkYsI791Vgqu94xzsKYw/VvThvmQXzTMUkMIfR71eEJnCseqYbQWfzrHfZi40HBlUOO22uvr3/N0EY+wVBzKy5zWzXraxpZqMNexhhcR4deOCBRd2nwppYzrKFImISvXqSKNTLqWhvAFv1ggsuKCq+jpOS1GXt69dsCTkuBN8F5GQIxoF5pZaDyDEE24QlJsMf/uEflh2Qv/d7v1fMBXDC5JLRLXQOtKkOjADsP/e5zxWzYHNEGIrSBGN+D30dMRW8i7FtiL6+N/wCj16JfoEVwwCS+MMRd9uY1A8H4AX2zMvLDyFIfXa+yT/99NN7JBNvt/8QeB4IMcCfJHxSkHSDZA7PQPQOzTjzzDMHR3VZUdAXCA9B5V0phD4YcOUPcAETxOhqdSICbUppPoPHPvaxxe+CGVjTNwdgkkS8ELioQ33MJ1oXxnvkkUeW0OKpqamySlI5jGa2skoQuLhDzJ/w4b1Dk/th4moz47j+XhEMIAHa37r7kUCeHYNQRrb3IQEnnyup/4xnPKNE8EE+6iciXAjhq1dKNZiKyWF36aWX9t71rneVcFaIj+gRgfzUZO1KC0HwUsEK+UoNgWaAQYI5xsvBKW3cuLFn5cX5CE4V4txswmm+c5SM2fyog5/lda97XQkvpg2YB2bZPOq/Jub1ZoGTV0fZh8Y4/iFxdtynZOwZQN/Lem0s5zwsCOSiAPD6IBySfyYSpBLCJp96bamMGmof+j777FOQy6QjvvkSIEJWFjK78kA79OK9731v0S50UcgqDYNEw2ySCCq7v+qzgRumiCmAX5oJz372s3uPeMQjBuv6NKSF+mUS9rQBKy20AaHctmeL6HTm4jxw4ZeBmjsELmyJ+veN+b84cXecJ2+cGcBgmS+I/wnBYc8ycfEZydmHOCEW4uThf+ITn9j7sz/7s7JMBZEgVEzcvOaoSfgq4MEn7TmbtMXjTYKpH5NB+PNta14dXGGF+vNb5sqckcikNE3qj/7oj3qPf/zji7/AAaRgb/6k+cJUHRiOj3Dil7/85b0PfOADvenp6UGw1IiMoDgH+2UODU3gnX0mQMVrX2YpI1n+r5kok+Vvt6vFdeHoW//Rj370ugDgs2OCTjVZkUYK6VWGfS04xm69t7zlLWU5SNAKgpTmgzz9vhR7Unnr9Oedd16PtEL8NA1r4ZyMTYk/IjKV/q2lL/DxAd9cERDWSzJfGSs0tgV/6lOfKgzC6gGNCvHO14TKtpQXs2DJUqCTaEKaAKGhHyPMG3ryerNAi/UHBJ79LHwCn+W0FsMxjmkcNYBC/DyqQfxHxQSfGBN0Q0wC1WokhoUQOd149t/whjeU2H2T7TMfwk8JRTpJkPKiiy7qveAFLyj/RQryMbBnJ9K+gGRRvjAEMPehspPWzkx83vOeV8w4mpY8CzENaIJpwsGZRz7ykUWDE7vAWTkCE5AXE1gXuLs+cM1BI6/EBAKnSbGx0gTGjQHoj48An5cGAF8SACT13avS0xEp4iZ9ras/85nPLIgiWk4Em4kcZTKj3WKvq5fziMSh6iP8o446qmgS9qa7T6tI7UC5SVp8CHCist3TaXinO92p9/znP7/30Ic+tBxBjvHOl8GbO7jB6Sg242Uve1lxED74wQ8uTGDE0RRiD7ywTPiyYALHRXk4jAGMDRMYSaKOCIBRsxfJH6rSFgE+Abhj+8QPaFXEbwJNnkkUQfeXf/mXZW1fmCyEgTyjEr86MRT1ch46lOKII47onXvuueVcP5II4ac9OuqgJ/lHgwBGnJKedJbe9ra3lSVdexGo7rmsKu8o8y2vj/r5GTbGaoSoRcvEzBD10RQq6yzCLPBnS+Dyg8OU2SHiPz42bubAuDCAptpP8iN+jhP9A8jWZEIQKqlvMwgV8W//9m/LUpJnpALiHyWZbB/SRlc49170ohf1Xv/61xcHIsJXLxtfG5VIMUoXJnmHQCBhDfaBK4U4OQptAKKd8RvQ+DKoSDVZZkiVW92WFxPA9O9zn/v0RGQyIfmO4IN5r6wvmcD10c8HRfkN4df6+DgxgdGoYiswLd4fntK+w4/Nf3wQHLW/ivj1AvHj1NaR7VBzoCS1HIIg4lHtffVBLDYhVRDRCxrxHzKo10eqRISSd/K1uBAAe/ObjEA8h1WDN77xjYX4hXNz6plPn1HmCs6Q9pKIRQ5CDEYd6tRmZX3JBGgC08EErglcvxTOx8EyM4EjiwuWkWrb5gwAIMI+ujauvP2v6xM/lb9T8hupieUN3hyhnY973OPKJFHXqPwmqHKSCtAgk4TzWwt2/pw16OhfOc4bU+BHkEaptxQYoy8wy5RjXsnj0fdkBMw92pnVmPe85z29qamp3i677FLmlOSWasea9SpnOVdwkgAyviW+B6ZfZV1NJvDQXB0YByawTRmAaKkIzLk2bP5DQ0U/JTguzASsKpvfZJL8vLZ//ud/3jvuuOPKbrn52PuIAoFTG5kQRx99dO81r3lNiUajVVAxIVnlhOvaWCbjYyoZqw9m55rSbiw7Xdkpc2McCJN/wIrMSSedVHw3mAIV3jz71M6jfD4kvvI0Abs2BXkJWR6RCZS2A9f3DR/DFREsVCIGI0x8RtWoHOdiZquSsovZYNaVoZIR5LNPAORD/UmxfFLNlJxOQ/Lb8/3Upz61qOhst1FUfkTtgxAs98RSTe9P/uRPil3JjkT4oyBMjm8crwh/2Bt+mUzgkA62cez/KH0yFozNmO03QLRnn3122fNhORFBj4In2sZc+ADgyWtf+9oSRmyFwNLkCAyl4HjfxHiYiMGkhVHGt1h5twkDyAH3Y/v/PoC3PiYs7f7OsQG2iaWKvfWtby3n7yvEWTfKpCJs+SGELauvfOUrS8y+uHBq32ohBrDB4ATRONYsnFC9qampou042MMOORoP04n3m5kzChzVP67JHDf3f1gWdsiL8dMUpVrilVd9fEEYiMCyY489tjePZcLro80NgfNbAs5/sC33Diw7A5ju75kO+2fngOdXAgA7BlCrw3shJmQWasvOe/SjH10IH3ceBWlzIpVzhhzCsITEkYTDkyCjIAbkGNeUxG+5jB0rQMr4JWP0W6Tai1/84uLvcLBmxkyM65hG6Ze5tApkeZC5KOoPLDB6Y5+P4KBdqNepxELLA6+L32iEfl0b+OoQGxuIdg8m8KOkjRHqWHDWanV7wS3NVLD+yiuvvN4ySKypfzomZSoAUL2xB4FTwSzJ/fVf/3XvgAMOKJLaRIxK/IjCkiEbkUSw84wzUdgwolgNxA8uzKTcn+BNReDEbsX4IL6P3/wcjj6zWUpoM9+KZ6sBDsYAFiQ+LYfGx78jnFjINg1hhKW9ApMUOHZ2gt2b3/zm8gYoWmNlogH8MmjgFkED+wTsT4l9CLRg/q9lCxRaTgZA2/C5IYjP+X0PDCBWb+k1iSYKMou7F6pJDZNqkRQSSOrxgonnPve55ThpKhzkGAUJSkVj/GWsCa+XvOQl5VBT44O4pCGYNT+YguUtuxZtkeXw8jxhNsZDre6a8YCB8XMS0gKc03D3u9+9HGjqmSRfV2rCRqi5JUeBZ7SnEZnANUELvxmm2N1jWfBczXe1vZjPl40B9Jc8rg+O+fIY8FNDumCVM0H1HSMCbMhM7ReB96hHPWpk4qfqmXj2GzXQgZ+IweRT+aWaie/o6tg8NlYSnNrLSUrt93+YpuQ+hpo7GCEzIhkBmcdm7G0dScI1Vkt53kjsYx8H0wCMpBpcyLowSYxTeXAbcYlwO1pw0MRdMlpwOZcHl4UB5HJfDOwxgZhvCsKj6lS/jRcSI35eXC/lGFXyI37LXyaK6SBeQIQXoqDyI5bVljhJrVnb4srWBbNhxJ9jT6QXScdzzkRKuGWe1XI1VlrfLhEjAB1PPPHEQsQkuARnEh5tY04mIA9hwonKMW2JUP01dURRZ1xsCTwULfj1iDu5DM0sx/Jg9Xp7GxDannFseEFnSH6QPQdg+6lT1UGwbFjefs4WDqyUSJWALRPJ5jMZIvqe9rSn9aJP5T61F1GsJjUXbMHG2CTEX5uUowYzBZ70pCf1LrvssqJ5NeastqoVkc/cEwD8QfwfVkfE/cMHjK923OAmr4+Xw2ImjiTnU6qso9BCP+/ZaAXNBJ7O621WowB/qUVf0+l3SQB8pwAuQ6tzYIAhqou67ohuB3lATpNTS/w4u8l1wowgIfHcNnhYtx2lnlEAOg55OUpJ/2OOOaZEMo6i1uo/woC8Nj5hnrXwHoexj9oHY4MnPqT/ySefXDRCLxRhdsI58OhKyQRok6HpljJnnXXWKJqARsrKQODmXsGQ3rocTsElZQDBwTaE139LEOHpYeM8JIDMa9d5lBfizwg/hOu4JgQ7CtGaUBNocwhnH9/BqEEbXZM+js/BDtPjyaf+s0trkdh4kiAwAMkBpjzn6litKRkcM4nqbnmZX8i7H5mJtM5aJgDvaA9WBwga4ciWltWd7bTA0coAp+Btwyl4+3AKXpg01FJmQY+WjAGwYWJt01l+hwVAjgspxLtSRfyATu13aKc99xx3itdMAmiYBDbwt7/97aI5CPLJCamtY0FQ3YaFSezvfe97RfJ7gWkiXV5ruobRgjnfi1N4xEf01dOa4is6D1MRE0C4oiZFSNJER2UCmDDHos1kVpxuc5vbFCZaMQ8bAtZ2D94j/AnfDU3uS0vpD1gSBhBcazvEH6rQHWLAFwdCZTutdj/EAzhSm5Pu1a9+ddEEaoEP8yAq4hfYIrhHQAtHj809q534jR/87IgT1GMX2yiwUz4TJpqv2z7//PPLMpd7qz0h0GQCAsQIDytGzhqohaU6aEy0WFqEZWv3MFUw9LsllYdBC66PiFWZcyI242o0RZtuKTevR93GzejVrtu8efPMWkqvd3bYRNvHYOiPnW1RnTivEG0GaqSjrqYbiJ/a7/x9zi/2mPXstUL8xkvtZL/SeOZLsBAULGkTXtyhnlGcYjVzNc55CAp7BzgGne7McUwbgFu1MDUXGIldhHwBNpi551ORhMZfG3m3j7xny9+nqVbOUVHvTbJ0EuVNSnTcCHVFpyHgcaHG7BkAs95f7nUULQgH4DimNWiMoBJgZWKS+B0PBfAkmDrWguSnPSFYiEb1txw1iu0/e25Sijn/wAs7mBXqXysJzhAcIgUJFOYozXRUJgD/OAWZFN4ExazCXCvS9mgHDQUtbZI/aauibHWWRWUA1BTLFxEZdc/owaa+97mT+CGv5T5r/bgl9X8+xO/9b97wI5hFfZZ41gLx52yDo+QFmAg4/+fzUa7Kmz/+GMtj//zP/7ymGABYwR3OQKsDzCobibzWjZlVScRlHmgC3kEhwtKqFtOgsvz2MQcm9bgwJe65FEuDVfpIJeKsSxslnEYfCuBZ8stjvYZWARAAEipOT8iqddQ+4+iylUqdVDJcGYI+4QlPKHYWe22tSH5AQOiQ0tKfk4vES4ALIl5IUi8i4ATzGjXEwI5dCGNZSH+2RVkw5MEX70+Cf+c73ykMloCBp10wzueutCnBVd49wC/Fp5DPh4zNBF4fcyBQ6L6xKvCWPo0tbGIbjS2aBhBqTlnbj6sz/XaPDlP9W9f7IZI1a+olW/PpT396UfkxhQ7AlCHIp7z31DuT33q/IJa1JvnBKmBepMq+++5bJDXYLDQhfkjKg82sIAWZAWuJAYAhOGB+e+65ZzlujqBiHmCGNXA2P5gF57Qj5O0bcM6EOatIGSq8O9qSP2mtomxnlkVhANOh+jvWK2yVu0aLjvLWcKfqD7CAg6u+9KUvHXid3e9KAG8COL0EvFCtrFevNeIvgA7nKdXU23NImRrJpFwtIcvHEy7RLNZigpNwTSzJGWecUQ4EwRwRcQ0clWcKIH7nCAi1JrwqE1NA1pegMbQ2HTRXWbY122KYAFT/YnxGDPnfhNPu9gEQXv/WugGNGmV3nxdn8toDUI3TT1mA5+Sy0cVaNbsXl65hHq0QWWEPwYL6D6Ec6hEIUhX3b5g1sMKgET2HKunPR4PRVqivKwyS3d1NIhYn4N2StE1r/ebAB6zakvJwdmpqqpxcjBHwL1QECanYqoAYgd3DFDi9T3PtDbZ1pv+sW9R2VNJXR26I6xFBlA8IZEH8rdKf9KYObQ67X6APB4nlvi4A6koCGjBxYo6VjRHea9mmBqE7hrPiHoMZBOL4FLRSo5LmIBFxTQJzDHfvvfcu2f1fi7A2eOOGazQB51A6G3BUs4g0t7LCZ8VxTRMA045kVeBaNIbWIi+aW7AWsFAGsJ46ElshxY2eSFJE6qyT6k5aW6+2xkrqQ9xaBgDgHCleDWUiTEiN5qBzqylBGg5QDjoeakwAUbcRZxIvSWS5tSs/eJkX+Ugr82VZzByu1QS+fADMIkTMOWgeapgvWGIAlgMxkJDmpVzbnDXgvL5PYyeiObQXzzrprVH+Jj8XVDg4UFHzg/i8yWfHQC4ipVP1p7I6ecYZfDVIm702eGXFCuCgAjVMRA3jyDpW0xXSpORwWi0pnf+HjdNz+axpO/yUA7YryAd8wZ7W5iwGDFeZrraG9WE13Dd2WiuTy9Hxl19+eZHkNUzAvCmLodqe7kxG5nAFPMspQmgNzYFj0uB8YdpKrG2VTocTIpadrov1ybtHvrfHwOkw6htqlwCOgYrzP/7448v6co0E0g9lqUqcXXYGCnQh9WsdXupYbYkmJFT1kEMOKWHPOb4uhghuwnuFukJCmhj4dpWDoJZYtWkeKlXX7NaquyZOwkGqPCZMQGGWXbAEDOUFrBFiXmYzFb4BmllHWUuC8tw7Tq2+8Itf/OK/osXwCcxr2WfeGkDY76XBGOyr+uoLN+VQ4jdgUoMn1XIK1clAK7heyUNqcRI6jtlylNgBzKMDWJpdlQncwMS6Mu8/lbIL8ZQxB5ZNvQKb9HIsFmLuWtICZ4guJuDxj3982WtB7a2Zv1U5ATEoMBFv4p0DF1xwQe/UU08t8GhqZsPGrqz5wkTtdkX4CLtPS8OKuY/GrpMP7bmRtOj3qGleDADHiYa8xHPfkCb7REcY/50OiVT9bfF16kyt9DcoyO7MfqetsPuto65V4gcPCICZMoOcSVchOQpykv5f+MIXymEfDsW0W40PoQLxCrxNtUhNnnB+HPOyllPOA5yE1x/+8IernYLKMgWsqtiCLJALI69gqtuhObQXNPjwgP+WPk2OPBXzMgH6SxCI+Lwgwp2jw7SBoXUZEGlhyY/jg8pKmkhdRIwrpqNLSOpaXe4rwOp/gQlTCuF6YakTjbuYqTlArBink5FILiYEu55GYP8EadRXL5vNDX6n1MqTbpwVQIXFfNZyAhfwt8YPtgceeGARcDVMGdzMzVSo//wyfGOWXGdk6lCo0gK2RLs2Dd05HIlvS5ocWmLIg5E1gHA6WOK7ITiP13ntEQiDklvFAAClre6oqVQ33W9LkFFenlKIbntrxZppW5Wr4hlCBgeJBgCBupI81H/bpGlSDgrBBPhS3v/+9xezCqy76vLcvJF4kjnqKtPVt9XwHBxIdE5tsSmpHXXBJmkDQ+eUtevQPKmvI4kQFBuwR/hwDom8lgVbl9/nqm9UBrCuv/Rg0o/uGpwG5aHW8HQ614+9RO2pUTnlAQj2KvWI/ZmIP9dg1so9khs8McU88aeLmYIlKcXxJ4GrMqSU+rxOPQOx2uY16zGPtLlPf/rTa94ZCJ5giaE6/cfpU14sW2segSmacKjoCSecUDbFVa4KrDNX0faLN23aVJbkdUV/atNIDCA4TJH0If2fEpznTjhQNNQq/UmVq666qidG/WEPe9hAE+jqIASlktoh+PKXv7xIHN5SwFrrKYldYE6NNgWW8oWaWA5Zud/97leQVT2YQsxreauyE5RInzYGAPZZn+UvqRbRS+ZV/AU3SX7xAc961rMG4b7g1ZX6hFzMB8wVM6mAa9ECIt8d4/zGw7WRNNrVXj4fiZoa0v8FfSRpLW/gpIsjkag3lpBq7KIsxyZ64QtfWJxcADIh/hli47XnD7FXvQaeOdl8MFITjmCdiGY/hf/JYLLc7Kvn2tW+1RyMA7OepBlNgLmLiJkCtTET5oR2a9chZ2Is75UlRfPRkXJZ8AWBExv6NFqtBbQScLPhtC/C3jgspP8do2Okf6vjj9df1Jhdfpb+au13CMYJIu489kAXEwLCrfUEGRDaFVdcUd6JaK9++laGwUYZ0v/HP/5xea8CNbM5D2CNuYppt6eCv6VLC1BGu9RUr2fDkJSZpBkIMKUQslOVre8HvXRqVUomXB/ykIeUcy3MWQVc7Q8QIvy7gReHqmcULaCWAQxs/+jkn/WlfyuXMRgD9/ZZgTs8+Yja/bYEYTGOOFOw7BCcnp5e80t+CS/IwAyKk2EKwdbAU1nzIHqSH4UHf7aH2X/zQ5LzLdQgbCJrmIPFEUnSNTWL7PNavIIDU4CD1hq/wCk4DbfbEpgSdFYBhHZnuS6TLOpMLeBP1T+KFlDFAIIIi6QP6X9AIMddYyDW/YdKf50wYConR5Utqk2p4/lcyUAhOaZhkw+HSq3DcK76Vts90h8hOzQlj/xqY6jgiZgt/ZFGWWY2XCAsOIsKtEIQL26t0gIgqx1xGLzlqxokn932av1vXsAfYxUgRMuqZaz8MmItRjCvMABxAXdDo2CaNNsF3yoGsLkf9ReIcmS/QutOQ0V5k2NREWscVepVjj3qXW02++Q+gX6ba/oCoVJyx+RWIRN4gr0w1TPOOKMQOEKfK1HpMQiMwhuBMOLmPM5Vxj15bOVmjmDy6U8Yln8t3WcKOBVY8NqnPvWpqgAh85zm1WGHHdb7wQ9+UDMXaLGsBSeNJs12wbuTAQSy8fKL+rtfdM4bfekxQz3/EALXM2Anp+SLEkmZtkQ94jB0tJcdZzjgWt7oMxtWCJI/hTOVg4mUgCxtKZHpIx/5SMmWDGRYmVRRMV/I2zVnnutHxKSXk24EJk2cgTdCF/xJ/rvd7W7llOu06bsYq3KYqQjPww8/vDBwtNFRTnSg4KAHotXoRVV0YDtV3jgWjT9LxyKVPb+NR1v9hBTZUa/wrlF75FcOAlJBXWsl0FaNr9I/4EGSC/219IbIEHN/PuYcdZYRWPKmN72pygkLWdmtb3zjG4ujMTW3ORvo3zR3pL5IQinnvv94zV+YSeJXCESMtYYezKv5JUgPPvjgEqmJHirS9crGHDyrIm/J0soALCuEKnFdcKLbR+4DIFWkVtsfcgoOcUxXvl66DVFVCGlwOBLOEggkJIG6yim7FhJCFEvhxGPSpMvznzABP44/G4bMSw1xJsy93NJ85/+sc/bVc1qAubb2vdbPCpgNH4INY2UKPP/5z+9dGbEY5qJPS7OzD/4nXM236Nk8OGSQYe4fVgQ8OQDNol00PHfWmbutDCDO6ivPYxBPDC5/s0Aga3FDy0CwRBiHfKYEyXvDOgJIVB57o5NDdgFoWF2r8T6EEUth7Z80IVXaYGoeSAyE77g1Zhj4diV1Yrw8+6IvLQnmHA4rqwxpxQFoV6LVgK4yw+parffBxxymM7aWsSonitbZFwQATauDiZcXiqBVNAueScPDYDuUmKNAWfqbnvEBPKlPkEPz6xgp7mw6J/Qm0iHutqTeLHfSSScV6Y9jtiF4W32r7Rn4IUon8zryqwMByvDlgSy2TV988cXl7Ug1DEBhZW0QInFG2SWIKYkx2G+//YrjypxO0gwE4DLiF4HpCDzLreDTJeSUA9fw7JeDWJIJdMC1LAlGnkM3VYQHD6XOIPyiOoQjbp+QyneIztr006pO4PwG6tSYGjUHspH4CP7ss8/u2Z5KnexiGh0AWFWPwdT2Xeo1Z1sXfMA0mYb4/tw/MQpMtcHR6G02JHqX5IGozBIxBqRVIviqmohFGEwKNZuvSPf8P6zqJlydwYApVyy1MgOuC7raNdqxVbiXtDxXO0MZQGYOhHpy/m674mheSU1NtWuvS01VF2SF4F5nZaOQPeaQb5JmIEBCYJCS6LAuQpQPTM2FaEHvuhc0NCpMaQuYjUMuRlkShNQiPtmtk30bZuPGhJgtwdLivLjV/NRqAQlXphlHMJqpSTW0OycDSOdfqHS3i44/rK+qzJk3O6JTV4aDw7p/TYiqcimpSBqczUAnaQYCCJkW5aw568GCdBByjdQAx81x4rLUlb9kmuNLW+bkoosuqgrGMpeYvpexPuUpTykBS/pvHJM0AwGwwMQl2pl56pofz8E1X85CWGIcHSkjAx+GhgMXhjoD5yTqdByE9NkvOnzz6Dj1f868OqKTVH+bQ8SU9xlGax/lMRD700kq5ZgCkzQDATDFVDnimFTs8i6EgWCcfw74OO2004rtWGv7N+GubQyA7WlJMPChSlppX9mNGzeW6mrwoNnuWvgNxx3gcuyxxxaN2Rx3wSnhKuBKYm51JM7A69Bu4MN+8iZNzy43J1FHLHFpIbj6YzUeaSgb9xynjzIlJJQEgDxdNqfn8uGEUu3SVsm8Br6o/kJyOY4EhHQRP5CYC+X4DMSR08RIj/mmnEO7BGvazzmdmpoqLymxHGwtu49D8+3GqiqXc2RQH/3oR8vYMM22lAzZ6UvPfe5zi8lcYQZEU+WFJQepO2l6djtzMQD3boh1y92igvub+EhDI/88TESxDxoCargt4XgGIOrP0eB5pFUXINrqXE3PwI92xIv/uMc9rqh/XT4VZaiXNqEwqfL16jk384EP7YEWQJugVdAuauZWPmc/SOZ0IX2YT7/HuQx48AU4g4HfC1y7tABlUmPmC6JFVPgPRAYCxf3QclwR5U3o/SY3omPF6xQNUv89J0KGsigdoSIeEuf81b4yyoAg0ua+nQpxuxAr+rCmUqqF+U6+Lvh4jvBsn8YAHLpaoSq2whTToUUw0ywJ1jB3c6sch65NS1YE9GuSboSAubW+L2jK7ssaBgmu5pODnVYoxqMDrmjWNmEvE9lf60nbN/ZkDo4QqkJhG9HJR/WR7iZMolkB7uXgDhzf/vAuVTER1Zqmk1Cpt8M2qDTbWSu/wYfzjfrsyK2pUKe7TCplIJF81v2lrnmogac6SZvcJcgD3cWsE1HhgpiAmmCimr6stjzmapdddikrLaPA1aE6Xv9OO2R6dyS+AML1kfIlbTfLbEXc/bDBLVSGmMg9+yrEVnm2KhwIgnghBULWWFeShyQRMGRrq/P9F2KndrW30p4jIPCRMNUu9VA+MJXvyliFcQqNjVQQbDESqWOXoIMtIB2p0zXPxmBOqbkcgqQVHJmkGyHAvHKeo7MDRXnWwDVpx/xKNAmwbknlVWJoOdKuivRpfFBkK+JOT2FM+kNjwmAhZ+DQFkwq9fAZz3hG2bpr0ts6lAPwCm/LS7e73e2qlpgGvV0DPxCyDTyOlraG3wXTJkhsOJGYZX3m3Xw8r9/mM+uyTRvD7zIFlNFvB1vY0y6WYOIMvCn400Sz76JmnsFVGUFatABadAqLm9Ze7qBdqwEbYg73cSdpvDyNryYDEPpb1P9oaN8+lx8q0nEfyCrgY3p6erCOr5PDkjqV4TPgWKICGfgkzUAAfEgCzlETTDsy4V0wVeYnP/lJ753vfGcvXhrZw2DbyowKb9KKo/YNb3hDmbsaaaUN42GvknT6NNECboS8+QFXm4Tsu6h9OxN6gRd2X4oRQU99Wr2x8q1/xeNiBpSowD6ND4h0KwYQ5aj/t47rffvqRfP5VtWyDw3A+rT1/wp1pCClfBxKUo3zY6tGV/kfSCHunwcfYqTkbRu2yUWQeeQX5ADjxUzqS+K1JKjNLgaTPglanmOxHHIJWSfpRgiAIzveoR9OVKqlB7C3OpNJPcNS5M29AfcNM/03Ih/kuCkDCCleiD0kzn1Drfj1qPT6+MwYo7Nqz44HNymeXmv/XSqMMpBIbDn1PyPbZlW9Zv+CDzXZWYhPfepTB2+WaUOKhGnuMhMtxvbvIs5Rgaw+ElywlqUrjr0aLUD/qKiWrjK5N0k3QgCTt9LysY99rDhca80rbyFy3gYzoI2xBry9UdhxYb8e81icB0nrejGQ8KHKF64QmR7aR6ChYsRzxIzoqYYVGxSK1FCG2uLlFBxLi+WouhGcK/cXQk/JHRNUpEEXsXiOEL3f7/TTTy9nKC6VSaUt88xhVbskaEy0RBrNkUceWcpNwoO3xlHw4etxIjPfTxdjRXvJNLydydzz+XTgipOCCIbCiZPW9WTAAEKapzH+oD4iDp5t3eUZ1V3HJeu9HY2XfNnxVP9rypSCa+TLJFKT7foTT9GlUQELmPIRNCPKkoksNti0hWGLRqvdJagP+mNszgowJgiurkmagQBiTgnODKiBDdrBXPl7pPw/U+Oc32kGPMjTBq3PMIBcGohlmzvE8zt3MQBqigimgw46aBBw0tZxHST9hbZap7ZbjDe5rcycw1ilN8EHTDFVb1BiCkCMNviYI4jDfnTkl6Uha/ZtZRYKPgRsl+D5559fAo66pJX29Ec5J0MHng3i3xfal9VSPrUkB+BecsklxdTqMgOMHX5MTU2V3ZfiCNTTkpIB3DnMuN+RL2m+lMqlgZise0XjwraGLv8lMV8Za86Qrjb4BwPg3bbkwVFlAJM0AwETbq2crexQjRrpr6RJd+SXFQDMwNwsZVJ/zpvz7TCsDsQrDICWYs4tbcKb5ejrUsJhses234jZyli+ZKVtLjFVMLXMOj09XeIz0FdLonI5I2D7qHdP+ZLmCwMIIk7MuX9fguT/m9SpYxBWwtUhQFtn5VMniSWQJFO/nfy7Zq/gQuILCxX3b1K7GAB4k76YhkNU8/SlpYap+q1SOLPRa7At5yLmvsY4dA6VwzisbHAkTs4KuCmokoBt4uqaR89TAxSAJynfQYc3KBd57i9/0jwGsG5z7Bd2Myq9d7+SoUYagocEU8GxeCJ1pK3D6sMweKo/85nPlOCflCLaXOvJxDGHaFIcqh2TWMAlj3ICbEhi3n8SYblSznfgTelv/h/Wvuf8Bxy/Dri0ZInpdTGOYfWtxvsJCytrqVl14YLnHKxSlm+BTb5J+F7y9Gl+XWEAbgRnvm1MlDf++ls0Az+aSYM4vtNMBCKIS4Z4bQiQyMpncM4555R3ppFwkzQDAV5xjtHnPe95xb4Gmy612nNMA/FLGGoXssy0tvBvcw1BSR4rD7kkWIGApY9UVglDSKlXbqzxL/PO/GMi14ROmwfzbgl+n332qSlT/ACBJ3cKX9/OfXCvWx8TUog9CPkugVi3iAxbBQrMnhcMwKu7eCBrlv+U11m7wiQIP9EACii2+mL/U+u7CMlzc0D9zkNUaWRtTHirhhbhj/mzdk0DEbeQJmFb1ZgWJBcV6Oh3u+DgwiTNQIAg5SexEvS9732vwLSNqZtvZWiOTCsh+fCipQwNYAsaDxy6s1bR/vpcE4wK72GSIvHOzWkCaDQboHo0/ys4V8qOQhZJpydpBgIkoMk+9NBDix1PKoJXW0qYb+5vpe7K31bXQp7RAiwJ2sxSs5tNW5gXJrf33nsvpOlVWRZd5VyKtUg6axusPOApElfqYMQQ63o0Hu2UMEK0vz6dAfHwxthCtc2RNEDaOP1ViGeXtNJBSO6QCmqujkKcHOgcTayZW2BHg3KQqm2z9oeTrG2wSQJiTtlKLRx0Wy2nmkevwBYTwIEJEbuQ1tgwOU7LI444ogSFKTdJMxAAG2a1nbLmtYOgB2Dji5MI174QHzwb8qPQOtpfH5KkiOSYnLv0CXpO+19FOnT11Vf39tprr+Kt7kJYCKGMd6LZSeaI6on6PzMl1DVxEdQ322a7YJkTCZ7UREEj1L8uJpzlFvsK0dKX01wSbGMCGIBx2j9isxNGxgzYVmNYbJgspL5kjrvEBrlPfvKTxaY3123w1B54cq4SBhztyrSk4gdA6/Kg/ULssVd4p/g/1W9sKAMw6dbyhS6axJqJU4aUkzo6V/KslS/RcYiY+u99CIgJEgxL5oY2ZQnN3ny2NImxLZP2rVw0lwS7ENYYjZUPyavOOBHBYpJmIvpohZYCxQN0MQCwRIN8B5yyWaYFluWAkHh++wjGu418hdhDdZiKylodgCY2Cdh+ZKqbxtuQ1jNqSToAu/K3dHxVPUq4GJSTXv3vIhzPwZyzxyvUSApq4rZM+pSefFFs/rfhg756jgGIdxD34GUXEwZw4ywm/ARMdSV5aQC0KJvrBIR1mADFERjlbhm0LOp3hgHEjTv2Cw7dANTsDNuvK0EGDIPPAEcj5Wo0hq56V8Nz6r+YCC+LnJrqPvLLmM0Pwvnwhz9cQNClMSwXnMzvqAeH6htc8JIMzuTFPr9guca+2O0kQavXpjlzbN7R0rCUdAaPpK78kcVKgHy7lfy+opI7adxPX8NSevARc1unsryGOAA5NcQ641ZrPUH8dHw58ov062KMnmMaTKkTTjhhcORXf862KUjhBF+EVR7HkXeprToLL5gPHMlPf/rTi0+D6luDU9t0sMvQuLmm3dH0MMbUuruaRl9SBQxLRGDgzo0MIMr9br/gUCNURzgZpqenyyoAYm5DQPWZaDEDIr/yxRall2v4C8HbwOMNSkKpayR5wtlBoRJmUDHRywJlc0wLIMlHWRLUf+PauHFj6ee4jGdZgNbSCLpiHnH05iafLthgGlYPanxJ0XSJCIw6Swhh8QHEn6mWPhVkY+txMuy2226DMM5EzGFlPefplUi9roEMq2c13ccA+EQwAM4bErQNjmAG9lZfzj333OKAtQQ3TgnSOobckqBjyWvmOrUAsQRMIecZCg9e6ziCmHN52JyDU1uCO8oIzGKK1e6zSJpfH97Am0clO/cBP1QDSCScClujRm3VaXWSdpnW+uSaTFoUh43lPxPXlcAM7L0TbnME/+Dy29r7P1efcyyWezEoY+2ab2Xg0iMe8YhSJWRuY4Zztbva7oFJqv0pPNvGCF4YMKbhHAmBZfClBfZFA4j52Rntrw9u7QzAW/cLDGUAyYnEHtdMrjzUW6ecSDq5lhP4QvY8Ro3NhpATrnPBRhnIYJ+/V0qDPc9/W5m56lmOe8wAB4C+9rWvLUvFNWYK5IUjlpW9/hrypn9kOfo87m3QuDGELqYIT8DbWQ00AAygJaUJcGu0H7i0fqdo4Fe7GEBWSALVdEgekuDKWM4QqdSl6mb9q/UKvgk3di/C7sN86JBzYnmETz311MFJQUMLbMMH+pqMiabSNTZdBY90Ij760Y8uTs5a7XIbDnXJmwYTyQt3augmYU9ASDkP5c9NvwoDCNj/Ktr34oDbKhA3hopoE5USnIOiJqXksnNQBGAOqqbsastjgqho1HjvUKCqpZrcNtaEux1ikv/jmvSNFiCq8cwzzxwcVpmmwbB+Kwc3nBMgqMihsR0SbFhVq+J+zvlUmNq058STLoaq3I477lgFg8hb9gREnTvzMNgGjGNbApwTwzCIJGBLPl2d8VwZ9q49AJC/CxGqer5CM4Ev1ZZ65my8GniAIbWOD8WBkfe85z0LMowzCPTZngaMLpcEu/oLNswAmqWzAkRHrnVnIFqz30bULfMPLdUkZaQOs4EGUJYCI99O62MChAFLQ2MAdIDt6Sy/PMjBxLUlzyG81OGUaKtmVTwzflFazlAAQxPcBT/EBO4IiRllGRWhjHvi12DyXXDBBSUGpHbujddJQwQMTSIdYeM+3qXoHwJOjTGDpMCnLXmO+Urwq4NplMrQPtZSYoJLySFfkBUnErgh7LCrM1kNdU7qQvbMvxqvYMWuFdjx2Mc+tqzXIuQ2mCiDcMAvj/xCFB2TOhbgo7JOhfp63nnnjbwkaH+DF6JiemC2lpP5J3Rp0V3zDpfgDCEhHsMctOFXA663wQCsArQmHRDRx8ao8e5mZakB+F/ZoSy6qq4IHtx4yWuYpzzy22b7vve9b9mP/Foo8FNTsSTYtdKRbSXTy7MC4MtaNRvBIukFA6hJiTP5vo0uptGvc0e7g7wFqLUNnRHRJ3BlFJXOdlepq/7WxlfwQ+NmMjm596ijjir757sIQhmTh/t7gYrEAZsIMe7g0E99x+xe85rXDJYEu4jZmEkugWZeJeYAWWrwWsSd5lwTvM3/w+YfnGhNzodMs2FYXvflj3pvxQdwy37GoUa9DuDqVgBqGID8GtCRRmP9ZtbOBVKDhcT+r4EduJH+HECve93ryoYZTGMlJWNOCVS7S9D40vZ91KMeVcJgOU4Tfitp/IvZV6Y3nOhK8sAvQprW0AG3gpRR5hZMANuA1T+UAWTjOExWnNd8lld1eUZq4V5Sv/7MsmauENgy6BOf+MRyCk6X7Z+AAS+HQkjqyCXYfL4SrpiWQy5zSdA4uvAA3nBgcZRiAlZAlFuLKWFlP4Dfw+gtYSMPxyk/AN9RR/4SCxBlb1kYQL+SoQwgK1M5zp6dy8ZnX+XHzdMH0JV/dvnV8N+YSXLRXF7iyEPb5f3PMk5QOuuss0psNwmQ8F9JcMHsxH/YCJYHh3bhgXFmOQ5TeybAsKvcSoJLTV/BIcfMvu0JAgAAP41JREFULMrfbWXlwQBoABWCptB6tFM0gJvXNKBxDKAWGdVJCqzVk18xSgzQ+r1PjR0PZtQ44cJ2g23LI7/akK3mGTyxcuHwGLsEaYM1JpByYOWtU947uVIZYA2MavKAYZf/JOuBcxhmTYJr8bk5DSDPZu7UAEygCVK4K5lEna8JHOqqa6U9Bx/OP9KP+p8beNqYpzLgi1B4/nOvQFuZcYcLAWDp2FJm7S5B4yXBhLU+7WlPK4yQ4KnBuXGHxyj9y/Hyo2EAbXTXxBE4VJGS1n8FA9ghG+sqmI6drnw6lAxglLiBrnpXynOqGCSWHvjAB1ZpTeaAveuILCf+kpxZx0oZ9+x+whd4II2ySxAs4JDXX0vgsNYCg5ImMdH8XYAx5Cthlj6TJlOYq0i/zpsVBtDPkFxhrvzl3iiToIG1OHEARQ2z9Nc88qtrQjwHr4985CMD+NeqfoMCY/gDAjv6y5LglRHRWGPTg4Vyu8TJOJZPwXKtBgYlA+3Cn5z6ShottB51bocB+FSlUTQADIDTq7JDVe2vhEzGnWNuHvnVNoEIHWHY/fXqV7+6EEzNWu5KgAcETqlUuyQIVgkTJwdLcK8vtVbCsBelj/AIHGrHDW6VJkDpX9S7oZr45zMiHart/HzqH8cyJuCqq64q597XHvllHGDlyC9BNEkw4zi+UftkXBx5TqvxLkErHMbXhRfK0QK8TIYfJbWHUdtfa/nBbZSEAQzdBjy7olRHZt+f/d/k6ghiWA1q7OzxDftvrKLX2PH2t9ce+YUg8lXfq/HtSfCGM7i5S7CGAShn+dTxaQ4L4U9aS/hk/DSfWqIG0xH9RluEAufL+jpd+6MAX6chtkHUDmAYYa2U+8YrCsuRX/GylaqxmzSM0jZYR37bb8F0Wk0JEucmofPPP3+kJUGwcMYAh6AdlWvNF5DmZBfDTHxBbxVpJvJv3bpfCgW+ppZAaxmAzuo4aWgpcK0kyGkN/ylPeUo5JBM37oIt4gCjiy66qCwdmsDayV5JcEXI3idhl6CdkbVmABg68faQQw4pS4m169wrCTZz9TXxxnjz91z58p488CY1gC4c6uf/JRMgXy8zVAPIykyi3zUdwgCobBVxyTmGFX9NOJFWxp//hw0MQ8U0hAuffPLJRdJhBjXwHVbnuN431oSHTU7se8yvJinnrACxASNsda2pemzzJA40o2/z3uxOJ1zdr9Qek9b/2wz8YljFsxtCzM3GZj9v/lcn7sWptRYSiea1znayOfILgnfB1XMS/xOf+EQBUS1sVyI8jZUz0C7BV73qVQOnXpdWiUmApQMvn/3sZ5cTptZSbAktupZRgiVY1aQ+bv4CA5g5tqflRKBETBOokTbE9kx+nXZWudSWv2RY4V9gAimd4/6Hf/iHA1W+bdzKYJBejnnaaacV6b8WpFsisyXB2pT4lGcFKNcG29p6xzWf8eb4aIj5u62/8pD+Ng/Bq6TZIWVSA/g5BvAf/Qby5pAyYSuENM+K8zpXZs+owDy/Us0A5qpnpdwzSZb+IKgdcDW2v7EhBhtlaA683ZjCak7wAA55M/A73/nO6oNDwYlko1kdeeSR5SUiJONqTkkzVpL8bqM3cJAH/tDSlenIX84EjGI/twpQpQHgKg74wGWyc8MmQOPy5BllNQMYVtdKuI8BeAGqHWzOTOhiAODD8y/un1d8l4h4q1XdVgI82voISSGoY79seCIoapJytKx8iUhqEjVlV3Iee0q66M345EGbcArddTEAZSLPf1gF+PeuBlTGE0u9qGEA2aE8pbSrfvlXajI2Ug3hO9a6JoEnn4ENMnbKOQtvrfhKwAezsyLg4FA7JjHDDoQtCI6xOitAjAXTSbnVmJqwYEY3/w8bLzxkQtJEazZPyR+ff2MCXD2s0ryP++qIKK5R7NT0AainZhDZ3kq5GhPp7+hzDipIDT5d0ilV2jzyq0tjWCnwqO0nZuedgOecc85IS4KED+3hMY95THl9No0Abq62hDiTXlKL7hpjkwHAyUq4/BQD+HFX5TrD5sJ1R2EAJkvKwXS1s9KeG1eqsLVHfpkY5tSVsTEm4/5HgelKg9Fc/YWsmJ40ysGhylk1EWQltDjPGJirjZV+D3xI8pozIeAh2Ng/MuIhKj/mA7iqD6yhQcSQFrd1UKNGSLAuolYmnYC1ZsNKmzRM8Vvf+lZ5oYXoP6qtiWhL+dzbcCWmwFpLYIDp2SU4ypKgcgjDwZdPfvKTe1/+8pfListqgx/6oiXtvvvuZXxJ4G3jBBsOQIlp1KEBFCRF+zSAH/UbcHPOlQCVpb3FZksk1thcyXN1Ul+c7CK4pUstnquecb+HeL3B1fl1xko6tcEGTMBRSCv11wQjhLWakvk5/7CPg52gyHzT09Ml72rUnuAIhzvncL6IpxMwkSHfw9EhoK0A5JmAP1ofKuyPEHgAdqg7FtBT1c2jvrs6hBh03sEWNrokA+kqt1KegwdYkGLUUePtSuAIDk4KsvOPY3WteP/ngg3itWzq/MNRdwl6+9Cxxx7b+9znPreq9gckjnz3u98tzuG059sEC9gqJw6lJkXeDWge7a+PH1fFjf/qNzCnBtCslPTSWFtSlzzMhqmpqZp3lrdVN3bPjM3EMIkOPvjgopJSTdu0HGUwDSbUe9/73nJUFjWvrczYDXyRO4T5OTi0uSRYg1uQl/bgvAUJXN1bLcl4JNGPBEYtTKwASB2wKDEAUed/of31gbg/jTI/7WIAWSlHYJeqm53QeYOQclDlzyr4Sng48kvqmiTPOf/4DM4444ziBV/L0h/MMD/moRUB8RC1S4LKgd2d7nSn3uGHH14CqfhjuuZAmyspORfSWLvGhXbBw9uE+d343FpSYQBR5qdof31IsV9EAz/sYgAqdbgl77XGaiSXOpMB6FC/jZa+jf8jk8G0ocK/4AUvKBpODTzACww/9rGPDQa5GuAxGMw8f9Cc4Mhf//VfVy8JaooQ4iXff//9B+bDaoAnPElz0uanrgQfCVeM1KYyMSUdTvfCAEKA/RDtcwIizCvbGgJYle68885l/dWegBrOpE7vKpNM9GqYIGNI5kcFJdVTGygDnePLJFFZvejiLW95S+9e97rXmnb+NUEEngm/iy++eCBcwKwtJU4KK95nn30KE1gNfia4hZg5APmIEjbDYAFOylgSFVZes2yoroDfla7JAC4H0EhDoY4r8XRv3ry5eBtxnbZJUp8yDrhwLj7bdzWYAQiZqiUYxek9NYwtJ8nhll4UQoNQbpJmIJBLgieeeGKP8wtTbcMtpeAXGCKSxz3ucSUUm1+mq9y4wxyNeA8nmhFJi4b6tDm06xgAByBnO/zsgEHRAKKyb6uwMIAo8K1+ocIFhrWUHJbHtqtT6tB50YB77LFHOfBypTMAMIKc3tvnmCqBTh3qVpkMk2LF4F3veldhGqlBDYPzWrsPT8BIylei1cKAhLTFmMTkVK3By9q6t0U+xMzM9lKUUZYACRapYvxlCTDyfUP+wgACiN/qqxrlvwdtiQTsSjqiTisBgmRwNYNbycmYEC/nEzW+hjtjGpDbioGoNxqRcpN0IwQSrgSFdyIQMDVaAHzif/HyEecwWE2oiYO/seXx+gVXUkjuuuuuBW/QEPgMS57BJ5qT1JU/slj5I5i+KX+hyKjgirhhW7D/c5oB2ZBCnA3ULxOg08OSZ7QGg5G68g+rZxzuGwvksoPtiCOOqDrySxkTyqb7wAc+UCayhmmMw3iXuw+Qkv06ypKgPoIx3Nxrr71Kl2lkK1nQGI80NTVVrm1fiV80HwfRctKDY0ui/pcdwEGXV8pXGEBIJ/sBvguQkYbWoHJLNl/96lerbXpleCallSz5IFX2f3p6uiBdTlYZ3BxfnpP+3/nOd3onnXRSCRpay5F/c4BocAvuESqk+YUXXli9JKgcLUDA2fOe97zeZz7zmRUbGGQshMVd7nKXImC6hAX8gpd26QouA7vE0QFgt/6xpU/jV8YGthI0sD6QueypjMq+1uecQxmAyqmw3l6TKn0bEWgMR7YSsO+++xY7OFWcrfs1/v+opHb9Pec5zxm8tqsPzKGd9xx8NofjVKINdXDokm+tfiFkAsZhIc5XwDzb8AucwBhMmZpOY5LgWFe5knGMvvQXjhEWD3jAA8r28i4GkGMVAGRfRIajtwxrCxqPtr4mD9pfH8EXaWB8qaVgeaRDAG3TgSWtPsMYWszkKEO1s4PLabAr0VNrciAjZuZNNYJOuiYny9grcEYE/vDq8h90MY2hwFwDD8DMR7IkSCOogZc8mIcXsRx00EHF4Wy+VlrCAHjyLW2iMzhWkzgNJYyvUsAUWkf764M4C8QD8F/qFxaHOKdhb3JyQi6//PLSWP4vPZjjSxmSz8YXye+VlgDWMgvirz3yy7iVszZ72WWXFf9B5eSsNPAsWn/hEibpYJUTTjihOLYyFr6tEeUwZ4LG8iwputIiA40h8cMKQBddgQcBjEk6WEbqYBhoOvcAFAaA9teHelpU/qjs69GBn0fDQx2BGsFpORu85aU2lh0xOM9NUgZhrJSk75ZjANmRX2LXuyRTMj3BGexZG1dW4661pZrDxI9cEqwhBnkQAE0LA2GiriQtgGBky+u/g2VqNExwygAgfgO02QKr4gBE4/H5urlD+wNijxda/DAQ9xt9tX5OP4DKNWLNNf0AOg7hhyVlcGdhjV7uYAlxpU0MqcTBYudfculh43UfPIyRHXvuuecOwjPbykyezUAgcYzGyBcwyi5BjNlZAYceemjRumgPKyXBF0vFGzduLPY/mmlLcAwDsDfHG6VSMLWUSfv/G2i9n++GwgA4A9wI4P+dCYg0lKIRAPXKuiPbA8PoYgC4mSU0nJnvIDm8hsY9QSJqvHVmTAAD7DPJoV33HDKaGAl8ahjH0ArX2API74xFkZPetARf2nAswZN5bNBSxhyslJQ4Jb4kzZ4+LQ4dgudOAJKU78hfIgAjz9/JPz1D84UBWHIpVB8PP9UHYjoG5d0qaSRtDcuBELuj4TJ5OuhAx0w5Wfl/HK/NPlpn7tJ2jAE8OHO8zJIdi+lNnH+jzS64Y7RUYbsEOZ1rmAAcY2pZdn7xi19cGMhK8QUksxJk1sS7uSDnubGCEe+/hGl20GFGAH5K/qR5GoCll6LyRwWfD+LGNmkEc2oBGtEYM8BhDFFR5+Q0yzg7z8klK0ELQMg4rMCf3XbbrQC8A8jAWVIe+bUSxpl9HpcrGPMVwbGzzz67rB6Ziy7C0H95MGobhCR11c5ZKbANvqj/NOrad0rmGPk5NscSM8d0h8mAlrdD21H2C4aYNF8YwLvf/e6y3hCq7nfi2ddxl0hz+gE8oAGw6cW2i0HukowmQAdtbsAAeMUtc9RMqPa2RSLJ9ZGN9chHPrI4Ao27DZmMB6JaMbC91bLUxPk3v9mDg+At5ZKge104Y35IRgz7Gc94RgmQMY/jmuAZlR8dTU9PF1O5C8+MhWBhgouctN9GPS2p2P/x/OthUl0hX9J8oXQ34hXMuXB6SRcDSDVXOY6uNqKQRzJxOi0eQKopUzJuoy+T4vQjk2JdtkLFGozRColXX9mplki8jYaxoptFyM6UzCXBGi0AXoG5lRuMe9x3oaIJ45RI8i4GJ58xokGvlG/+L3/m/koGcInHDVrvDRhAIx7go/1ODJ7NrtNzBIFIhF4KX+wzjdlZB/91WhnLgdbTBcgoP64Jsln6e/zjH1+iH2sYgMlk77///e8vTix2Xc2EjisMtnW/EHLiyKWXXlq6UyM4Etcwbm8SwsjNzTgm46MRP/3pTy8Ho8CZtjHCJ2OxZPjxj3+8aDoVWqb4f7j4ETBIWvd7QORhSxQdIjJ9LgD/b9GJDfGZMxRJB9lodm+deuqpRU1mx7SpIcogImaASXE0ljLjmpIr22oqAWBbSq3IuMCE+o8xGvckzQ8CYAeGeXAoQu4yN7WkHEKyNJZnBXAGtuHn/Hq48FKImZOTk5nWgum14Qw8TJ8BQcMUT1ydqzdR1/Xx2YCmA3aflSdp3e8BA4jfMHx9eBV/EtfP9u2toYYFYOJeGicpuzSAqLMQkXxpBhjsuCUAZjNycB5zzDHFo2yMXeMzacaTR36BT9tEjtu4x7U/CJnQyCVByN/FjI0l50PsBs86IsM8xinpI0Fq1QKTq2VQxm/zj+R3G57F81T/Pxv+Aud/ovmBNNuKAYRtUPSkqPCD/UqHii8EgTAcisHexalxs7bJUacyu4R3l2fdwRrjqAUkovAk61/XxHjOZBDjcMoppwyO/GqbGJM3Sd0QgGdUXEuCo+4SxDwcY0e9Fk9ACxiXhE4IUE48LzlxAGiXoFEGPlpFE2NSuTIVaFg2pX3A2Ps0PicDGCwNBCFfHOo68Tx0OVBlVHrc9c1vfnNB/i7urCOkpKAguwN5PknbLgLT1nIl/bHP4QlPeEIZm0mpIWSISmuw/j/KSS7LNa6V3A5CnpqaKoeF1O4SNF4EY+6mp6fL8NVTM5cl8zJ8paDZuHFjlWljPMrY6+BoeVGPHVo0Qt8OLUe5Yv/n8l8Or6kB5NLA+tj2enk09nkSPVKrGZBLLAISaoArj07zH4h7xs3GSQvQF4xpv/32K5tL9LVtXDkpjvzypl+Hn5BYk7Q0ECD5EHKXSaZ184aB22LsBGdmBE1tHBLpf2Us49lfQojWMKekHeOQwAD+taQtfa3886FpfEuRXP7LMlsxADcbZsD7+og/lAHID8BsGMddCQrCodo6pU6Dxb2e+MQnlk1FyUTUty0TgPKuCscMOHRx19JVY8U0REVyylDlJgxg8WeRicmef/nLX160LDDv0hzhWppnNE6pptzi9/6mNWJEgn8ca24dnzbdp7ebZo47KWjEmLznPe8pPgP+g45UDgAJvH6vfEnbzTI3YQBhKxXPXHCOC6JTiJ+rfiibgexToZ4JfKE6A3AbA9C45wb7oAc9yN/OwZdMS/ylTxiRNXybSRxiUmOTYRpgcNFFF5XyXWWWeBirtnqaGNySLAm2EUsTCPKZE7vlzKvIzm0tcOAMYcnxd+9737ta0BCulgyZmhWbf9Ds9mg42rsQTJK2m/C5CQOIh4h+XV9l+HTfDGjdmpScmDOwS2XWOACYFM7Ao48+enCMUxfjUHYpU44j3/bT1Zb+4uRssje+8Y3lHYHGNUmLDwE4k0uCwoMtCYJ9F85gAHDSaTlOcuao3ZZmQAoaXvzD461GuYxnfMOSMp6T+EwgGgOh01Ym6roO7cb4P9WnZQ79m2jzc7YaqkJZL4mGz+tz2qGrAZ7rGHv+zDPPLDEBAJzENGxQnuPozWOchuVd6vsAzHHnbT9//ud/3psKjaZGkhu7shifhENP0tJBAM5YErTPAgH17dvOBs0TJsCso3WKoe8Lts6yi51BX6j7krX/mgTHUtC86U1vKjEmFYImmiom0Lu0kTQ9u705GUB6CgNoF0ZnfxEVweybcI+sTAd59p1MautsB2cqxeTBxZx+8qxnPauoNdtqmQagfCRRihw0XQzMmDEw57F5uy2nZoVNVtqYfM0fAvxHzDNe8NpdguYWwTjP0uoOfw0zwBwud0InnHheguKgmFEETUZDVggatj/v/y/QsDEmTc8e75wMgKdwOvYLR6zxDwJIH+oT9FAGoFIDodJzUHCkdTkDlUFkJsIxTspvK66srzz/j370o0d624/+OiacNBIPATknaWkhgMl6z8QZZ5xRXgpaYwZkjxD8/e9//6JFwLdk+vl8qa/aS+nvtXLoqosJeZ6C5vTTT68VNCX4B+2iYbQ82/ufY52TAeRD1+j06c3/w36T5lYDMACOCp3uGhyAmAjnBDhwQ7nlts+SCXnXAQZQ+7YfTIMjx351y0zs0xrNZxj8JvfrIUCdl3KXYA0hmxu45kWkzDyONGZfF47W96o9p3YIO3tnXvayl5U9MRV2fOkfQaO/Nv+w/2sFTQ3tDmUAmzdvLlD+oz/6ow8GwC8PADIDZiA/ZKwAzEa74IILijrcJdFNnMmk+jvN1Vp6jf9gSPPzuq2PdowxRYQo608XQplMDC6P/HJSUO2kzKuTk0IDCJgbWkBzSbDGZFOBecO4bUmXMP/lZNppVj7qUY+q8l8knomVscomXqBC0FyPVgOPr4hNPxcbZ9Ky37PTUAYQGW8Ix8H2mzZtovqf1QfUUDPAxOBoznI7+eSTC3HUELN6lbNz67nPfW5xqC0XZwZgbVHj0yOLkNsYgDL6jNk1j/xqKzMb6JP/C4cABiylXVwDf3nMmx2pz372s4vpthwaJ5yxCkH6v/rVry7vlaiV/sYpjFmMidiZNCFaIJix/2cG4V+HhiPvUGdHGwMYOA6Cc70jGr4mAKiyViZgsNL73ve+AmzEkvfKgzm+cEZc3NZbyUT1Gc4cuRfvljYSoA9+8INLu1199RzSONz0Fa94RdmvbjInaXkhgJDtuDznnHPKkmCNzwlepcZp9Yk0RWApmZdqBLRMWovzIUKjrsYz5ZiZYb+X5UK4agwtifPP2v81aFa+Yc6/rKOVAaQzMMJ8vxsFzu8TZasZYKC20L7yla8sgUGIpYuo1Kuc120ff/zxZZmHWdBVLgcx36u+2Yxx5JFHFju+S/o328kjvzAuSDVJywsBDIBXnwbACVvDAPQQAZlnQTjCvZ34tJRaAOZCy4RnaCKPiO8ScClohNhbXuf4RCMdifovy/lodrrF+Zf1tDKAzOQagHtznyBbT1Yw4ORStIAam1r96tb5Aw88sMTgm+Csx/PFTvqJ++OqwkQxnK6+6qMywjFJHkg0OfBzsWemrj64gSByl2Ce/NPH0aGVKGfOOXv5nZzfsJTCBr6IOxB/wPPfhWM6bgzpmxL0xHFZqWVuUDbG+OahAJj1oJMBsCOizPqwkz8dlX8yOqbM0MhAAEYUllt4OznKeD8RXFtC/AY5NTVVouq8hy+cGEumBZgYa/hUQasQNQwHcJXDlb20gsNzIv3bZnVpn5mzXWLpmYSEZzXaZvbIvHH6it9Yqg1p8IX0F3fw/Oc/v7xQp0bLVI5mSbMxNi8+rcBPkX9e/X0pWo1xeunPUDpNOHQyABlDlSj5ovI39gsyRIY6FjCBTHbI6TwCN7C2lNwZp2Qr/cu//EsBRFuZ+TzTD1z/G9/4RtmNhZC77CtlcGXBJ3/7t387OPKrOdb59GVSZv4QAPsULB/96EeLal+DZ/IgRAFF9uLb/7EUZgAcY6IIdGMWE3Bd+ALPmDPe+POOd7yjSP8K4kdYheii/F+AaNJsF3RvpNT2nAOCD6/iPwYA7xaAZ/i2mgO4HwD4CBWuWMIoE6qcaCmOOaoTYJi0xUoImQTA+e1itIxnctragGgmFLJYgqLhUEFN2CRtOwiYMx/S0tzQILvmUm/NJ83U5iAaoPc3mM8uAq0dKbyAL8LLaYxpw7fhWPYL/lthcqjp9PR0wf+Ofl0X9W4XY/pqrBjMvIRzhiF0ImctVZUlQR2Mgb2hP4hWnR4ASFXvEcTJmAUIr4tg1I1RYBhMCHH2i20K4PaWVpxKVHMSi3GbAEwjj/zyHxJN0raFgDlByCQ6x2wHoQw6C8/S5HT0myU6BNuFn4MKWn7ACwE7iN8JUbXEr+30GfzlX/5l2cFY6WMqb/2J8q/Tra6lv2bXaxkAgin2RNgjZwVhfzsAaElwqPvbROCoPPsOyRTJxK6pAbA8Jsi7BHFnGgDALEZSdyKJzRg1TMmEYhp2kjn9iO1YOTGL0eVJHS0QMJfmgkPWuwQ5aOFKLZ7Jy+SU4ELiRkuTnY8wEnv9rTIwZTGpmnr1mfrvvZs0AKschGhH4vnfPtr4ZmhBZ8gbtFodk17NAKLeogVYGoyOvhqBRmoVgfLgsqLsTjrppOrJyXJUc5smqHbUooVKXADGhDhlnvnMZ1aHYxqoPjFLLBvVMjLlJmnpIYBI+HFySbCWASBK9jX89Faer3/96wv2OSURO+/SKUT6VeP4g9vwypFyL3nJS8qhNJVCphz6EWN5NUj3pX810EdhAAMtIGyTtwfQvxlEQSy3sigAFsEkbpu9Xbtei+BoEA94wAN6xx13XO8Tn/hEMQUWygRIciHHvP84dVd9JhRC5ZFflQcxVk/AJOPCIQBX4BmnnqXnUXYJks6i9EjrUbSHuXoNl6j+Voje8pa3lOhWOKx/XQkzUt7btjABZo2+dSS2P+n/jfD8nyZvauod5QaPu3s2yFp+DMKDgzCO76s1rY4GeUwIx9lTn/rUwYksXYSnNXkAj6eW0+1nP/vZgjg04vdCElt+qYwkR38MW4+y8S85uo1KvP8OcMDRJ2m8IGBOnMd42mmnjbRL0Pwre4973KP3kIc8pOAYU2DUBE9s9bUcyXS1saxCfS/NwHMEb7OPl5o6kAbNdOGmwvLE5wS/R7H95ZdGZQBpX6wLe+Ps4DxfxoGinlYtAHASqGeccUb1siDix9kRnddDUd0R8XyTss4ssP3Y66drVDN9wMUd+WUMuLLxTNJ4QQARJcGwoWuYuxEoAw+E6R588MFlR+p8zE14ok2xJVR/WkVNH1LA8HM57EOkoP6oryMVzz8aDOl/lqGMYvtn3Z2tZMbmdXp6urDIAN4xfaAPlgmb+fK3PDz7lvRe97rXFW8tjldDSADBFrIqwKNKvaJm1WgQ2X5e+SOcE2/Jp6a8PJiGI7/4MGgxxtEfc1Y7uY4BBMwJPDFHVo+o0eauZp6Vlc9a/VQsI6qHqVqb4DHpz9Fte3it11/9yjIxmcfnnXdeCWyCpx2JBIpuF+l/rLxJkx3lbvJ4Xgxg843RgbYKXxySEUNo1QK0jHjs+mPTZwx27QThitQqQRX8AThsDQPRrnzsfdGFygutpFl0cVkAVlZ7kzT+EEhich1lSdA8wwdOZ+dSONUKvtQmxG+52uYwW41pjOrsSnCfIHSY7mGHHVaYl80/FWVF/Xnd18Uh/b3woyrqb67+zIsBqCg4TikbRHRUn4ixzFbdGBFb0w9VpcQGmKiKwZY8VG/Aet7znld24HHK4Zw1SRvZDjsPv9J2WzIm9fMZCMcUMgpJsp62spNn2wYC5gbx5S5BTj1aQNdc66088CLP6YNvNUkZvinlnDqsD3CnC0+yPULR9nlmB4FU0VeIK+hHOy/Ux6TFmv7OzjNvBkAL4HQIbvkP0emTQ2XC8lq9YwZo00Z0uGgBAiVqPPE6razJxaXtqkp/QAXACuPAdLwvnqOolpBNLmnAAcjsqEWK2UCe/F8+CJgj9jyJLAKvhtnrHfyiejsrwLkUdu91LffCPULJuzG948JqlzrU1ZWUxZysjr31rW8tsSWV5uW1aA3Nxfj+EQ32NfKuJud83t3TOYvN3AyiKmwygH5MODyuDq7HQ9fKOnFGNhZTwNFMGe+Po3Ulk4kJ8OBbx0fUGEgXE1BOm4IyqGuQpI1Dq48NyDHjiLOpsAsxjUkafwiYO5qmCFTvEsxdgjU9h4OIHp6ogwbYhlvNZwSL/214lX3QDry1F4Xjkd+iUvX3pt8d0BqaU1/SYNY96nVBDCAa24IDRaDOv8fvFyE093y1pTQFSNa3ve1tA69nE6DDyuckeaFCOmvagI4bU9HYZhiHttvya1c/TD7O7jAGnlmMZ5LGHwLmliQV3OMQTas+ozgD4QcTwiu7roxXd5Hww5K2UnDJV4O/8qRwec1rXlP8Uc16hrXVv19e9RW/X4Tm+st+nfTWVudCGQAOxPlnCeKvgjNdGkyAYV5lCtjsAwgf+tCHCqBrAGgwAMYJKLWVMTlsK+aC04aohl0MQH2YBomvX5IyNWpdyTz52uYQgB+0PElIbeBlJ9OXVzl5mXvOpRDOi3kMS3ClL/TKxjLla5IyPP7OlGByEC4VZa9FW2gMrUU7aK7T8d7VnwUzgGjghukblwWf2Qc8JtDqZTNgao8lucc97nGFSKlFOXFtHQd46rnUBjicljRoLv215Vefuk067u/8NurZRP0HmZWTzDFb3J6NN7zhDWVJkGqf0rprJHCQhnmf+9ynHHE/jPlrJ/GVz6CrfnnhuI1Hz3nOcwY7XbtwMvqLlkT8wfdn6n+f5lppTL6utBgMoMcJQR2JJYmvRoMvQ3iRWrUAGQAMweGCdmQ5OaVtouQ1GTimNdeu96MBton5kz/5k+I8rHH+mQztiCuXMAP/J2llQcCcUcsJCs5mqYLQBtpf8+W18GguHFAfXCQkjjrqqLKXgGaKUJv5/aZZ8D8RLCJig15GiSnh+DOEl6ExtIbm3FhoWhQGoBOpjsT1uBjsZUGodKfWTgIgTs2zzxtqOQTwqEhNIAIgZuFDpQ8glKAgttow21zdyZEt0QBgc1LmApzn8nnvnKOY+AxoEJO0MiEAN7wU9Nxzzy3CpQYHcqRwId8R2WY2yucj+OeFL3xhCQXGBPiQ4KCPdi1/e/mMGBg4jzlhChVJxB/H32VoS/6ktYqynVkWjQFES0yBwqaC8J4CKJF4BVvFJwAxBaJsCfd1/jkGgOuS9uqRB0ABVry0ZRqrCADp2VxJfnHZlv5qN/Boy2QFgMvOvy4NY652J/fGBwI0PqsBH//4x8uSIJzo42VrJ+Gdspy/mzZtKrhA8AwrK6/QctGHzpd0Cpb4EfcxD5qtE4scPe88CS+S4cAehruNzqGd7bSLptwPOkFjrTQlX20qbvvazF35Qr3ZEhx3h6985Svfj4M21gUxTUfHmQKt7QAEYmYK0AKYAQ7qAPScNK8bE+PNO2uLpWfDODPJ7zkGICw0GYCJHZYAGeMh8b3pV3u1nt1hdU7ub1sIwKs+8RSNUih6agFdxKccPKS2v/3tby9vvaKVzpXURZpzHsI7h3kI7bW9GOE7Q4KzW120XTjWhouNNqj+Dvp8aey9OQdtRVxKp2ndKN/5c27x2VmsNYM6C4cKW+VzQVR7BuB0ujNsD9ABUBgn4B922GFlmYSGwHHi/r3uda9CqLjrsEk0yQh4lzgw0uSlXTYsv9FgGrQO5oUtyD649CStfAjAKT4dvgC7/moIEC4iUoTNvve2qwwia4OIMvAI7vwgDpBRHtFjDoTcMKE1R53F6x+08/nQSO/Tfz6grTnyz+vWcJE4r+pKoYEpEP8OQfxBeIi/c70SgQKcrb/sb2vwtkfyxiNoNpmJaSN+PSC5BVlYXaDGm4Q24lfGxJmcfNuPe5O0uiBA/a7BBaOGLyQ+qZ1nBdTEExAkBJZEAGEatFj3atuOouUFH33BeYi6phdZ9VentBQaQKmYuhKBNL8MjntYAOD0GDyPB1Ogqk0ESZK7InqTMVNFqX7ol/wmgffXFl7cF+d1f1iSn8bhFBeORcs/2nN/klY+BMy9j6Agpzp5iW2XEDFqeMcstDef/X5lePBJ8ho8XCDUrgvc3y4E0pMd85W0tMA65yw+nCrmzF5/E/FPB9cygADYmQYUpavjaRGfSeLJRcA1QDdhCJkT72lPe1ohfnW0EX+OCMfP5SJ11LSXZSfX8YaAuaQVOgU6dwnCla4EJ5SlRQok41NaBr/QNWgl2j0T7aAhtNTV1/k+XzIGoEOxVllEaHg9D48B/VMQ4s3i9qI6MYYN3NKfCeyaaM+paHaO5dIfM6SGaQxre3J/vCBgLs2pA2atMvHKp3O5pqeEkbV+b+elys/IspqSI+e5Fo2gFTSjdNLQyDVVFlhSBhB92IKDOUg0iHH/AGT6A+Z2p1Z2eq5sCDkdiDYZ5dtUugg5GUBw23LYiFdGUf8naXVBwJxaPZrvkqBVKQE88ISGuAQJjWzfp5H90QzaiXaW1A5dagaAg13Hhgnv+jdjMIc0CLJbBxsByqS9j+TAzxqHDeJPG89hknkMc9YzQvOTrGMOAXNKnbde71XbtQeHGhY8UX7jxo1llDQC9xYxlcr6tHEIWkEzaGcR25izqiVnAFplwwhfDNv83QG8E0KF4gxcVFMAV3Z8uBNdOPJqnTwYBeeQwyTtIONvmKTVCQFzG4RV3lPhpaC1Yd4IEz5NTU2VGH5LxYvsCyjr/WgDjaCVpbT7m7O7LAxAgzEw3MxLRp0d8N5gAkKFF4XacGN2GfvMsWGWbqh8XZI8pUIu/alnkTm7oU/SmECgOb+jHByq+6Q+orfBCCPBPBYpcfoJ9X0v2og61/dpZZGqb69m2RhAdGOgM4WD49Ex4MtC/WZMLdjDSY13RBgVjaOmJtjChNIahG++9KUvLU4ejqIuptEOzsnTcYaAuTXHlnlF5jmMpsZUbI6JL0CqETDNckN+/xINBC18FU008gxopXFvSX4uJwMwgIFTMNSqvQOIV8cVK523OYCrm0SHi4TqVBX1pyNJ6Lnrj1c473k+SasTAuYYvvABWPYddc5pARIBMmrZWRDl8d8haOCncX/v5XL6zepDb7kZQG9z3ykYMc0/CgA8NABZop6iY/N2vUc9ZVw8+LSBLjXecwRv15/3ye2+++4T2382Zqzi/+x5/p7cJTjKkiDGIdXgWQsI0+O/JXD3IaHy/3C5nH6z+7TsDEAHODgMuH+g6CP6nHRDXOfFBHBjiQ8gf5cbQ77kMYEO/BQYkicFDck+ub3KIIAB2O9vo47dpbUMgODwjggJznYJmrnA1sdxuK78vmgALSyX0292n3jjt0kK6Xu9gcfOwW/Gm3+uCEfIAZSB6Az7pypcWMcBEkHb9vnjH/+4LAGKB3DPs9nJfSqgUOHjjz++BPwskJvPbmLyf4VAwLzbY8J3lExgLpxB6JzMpP9rX/vaheAMSbUu2nWq7xND8p+/LYnfNG0TDSDxI5cHI7jinWELPSeAnAxgJCcIhwz1P3d8ce4hdB+Tlx//mQsm02m/3vVHEnAaTtLagQAi58m339+5fLbu5rJe4gxowJv8D6ccWpM4E467UQEGp28I3FsP18Pjf9ZyLvcN6+xNReSwnEt4vx8jcG1cjwoAnRjAZQpgTtX9Sz+ADT2CerxKzCRjDpJJR/iSQBA7Be06nJz4U0CyJr/gBG3QVnMHwDqklnkAZxB/4gztwNuhHvawh5XVIgLD8xGSzFsC/zYEbr8oJP8rE+dHqGNJsm4zE6A5mnhN2A3xws4NYZNdGubAhgDUdHBelIsBVDEBnJpKZy+2Q0WEAjMLcG7MwaTan/2Od7yjBAs5jHTYcWLNvs3+beLVBzmkRJTZ+Sb/lx4CYI+p+5h7czIiYZb8dozach5LceUgGjikTskeEWcBHHTQQeUsitQIRhgd4r8+6rPB5/gg/uP7uD4vf9cI7VZlrSKuqpoWnmldAGa95ZCQ3q+ICT26rwnoY5WpYvLTlnMYqPMDbOIQGOSIJsTPjnPYB8k/CrLIC8nSv5AMx31ry67JFBYOikkNbRAAewRqLsDeLj9JrH8exiFPzXzkvMIbmgCc4RNQF+Kn9nMWExgkP0FSU2+//wPJH+VOEOiD+APH+QJGUiH69S36ZZwYgMHpj48Xjjhd+MWjMoFSSUgCyGHXV/gZ3Co2nt1gNAJvixlhEgtxJ2J53dTsJLAEAtEoIN4kLR0EUstD9Dz4Yvu91Veyru8lMDWnRs3VQ4JCee+RyORgGv4lODNiKkQeOEztJ/lfEuUJMoQ/FsRvPOPGAEqfUhNo+ARuCILdEty62mTB2dl3TdUQB4/JGIn4ETTEIB3EC9hrINoQI7GSEFy9HGmu4/wO8mIEbMn0S3g2SfOHgDngpDOfmDoCxXS9Hg7MEajEo09aH3vssUUAYA78QLXMPnGGdqGM/3DGp7YO/Yi810fZ9VHPusC3YvOPm+TXT2kcGUDpV4MJPDuI+CSqVyRf1UxAgYUkCECTECvgRQ5HHnlksRGbderXlXFSjP0EDn+84ooryoYT/geMAMNRzySNDgFEh+hpV2DsTT3T09O9I444ophxVnAwh9S6MFzE6zDOpz/96cU8wJDNwTKmgqMET+DGc0Lyv2lciR9MxpUBlL6FBrBdAPDaUOmeEDfOQkjxsWbXecCoChaStEXKQzqvFOckchzUbMdhIqm2xJZTQx0s4vw5TMCJxJCBJKIVyO8zSTeFQDJKRA/24OUIL8mqTRBSUe/BFeP1XEp49vGjnMHPB2SVh3kA9ll3KbB0X+W8C/2J/h1qebvv7ceBxlIKjD0m9gF4bZwtuE8A9gPB5dcHxzfzi7Ydaxg+sPtJf28h8h6CYW+aJYFMOsQlhaipfAWWG0855ZRSPfNBxCGk9VkmhBw2tLG7D37UfMySw9YWben5z39+7+EPf3jZ4p3n8aVKrszslHClCZx44onlXRPLtNz7y5j7HQIXmKqPCOL/UOLu7D6O0/+bQnCcetfvS0ZLhSbw+wHfjwSS7Bgc1lZiuwmXJJFAV4baySP8+te/vkikJPRhDXouUVl9MAwmAa1A4BFmInFSYS7yYwapos6F0KXAKvtKIgUjTNO4rc44bUfycg0n8VrBcYCnPE3/TRecwBV8re3vv//+xbM/DyfeKFC/JnDyZoGTNrc9dFuH947S8ZnFzlFKbIO8zb0DwVXvFoD+cHD4uwbh0ASYA4vOyEgir3KyNkwydRE/sJD+EoKGsKQQyR8MrHfAAQcU2/SSSy4pm1AwF8k59SnZMnCpC8FLwRX6Ba7gaYw87p///OfLSMBBaDaV3Wu2HMQpYZCWbOVP+JYHFV/pHEyGU1Fk1CzUeod52NLrdXh7B/H/KAXWqJVti/wrggEATDKB8An8MGzB349NGe8JwD8qAJ8BFUviHGwiHUSqIU55fDCNRF4e6elwYFlPfvKTn1yWJ9m3QktT8jnJyPqzNpXFSHz8lmraLhm34VeT2BA7JuhqTMZiB+bll19eeggWmzZtKodsIHq2vfzypd2uXHMORhmaepYwFbyL/jrM48IQFAeKYVlJxA82K4YB6CwmEETkkFEzu18sAb08kOsYBBKIt6jOQXWSzLSAlOZN5NafrpSMQD7IqB7I/Fu/9VtFtWVeOL4cQWACn/zkJ7d6MQnNAUOgKofWMzAXsh/jwBCyL8ZobNR1HwkR26DFDMp04IEHluU7PpVddtmlrOPLn/ABo4TbQsfnsBdpofVk3xvX4uwz3pgXx3gdE5/edODm5s2bZzyTjczj/HPRVedlGuyMrj0TMPSYaPOcmIztgmgXzS8AaRAeKR07Fst7C0lz0myhKSU6BEoJiQCsYwtXdl6dNqnHmEKmqampIiVThUZ86mp+3MtPlht2NcYmAbflk9dHn32av5XTf3Y8Ym/a22x56j1mJjx75513Loe2qANTU64Jj2F9qL2vTv4bsPQyD3tDFvklr9dE328WfSZwDgnCf3dcB/hY289xybdSGQD4rQt/QFkmDE3gjoHIFwQx/V4gFO3AhOSkyDtyQhgcSRx4Xvt89NFHF8RnkyYBjFzprAJJfK4ICjPwkUhPSMwjbikSU7C+bXnr29/+9lY1/fZv/3bRFMQsJENRXyYEpo3mx7Ns129jkpRL4i43+l9JrPrl3IXvf//7pY/NPF6+yechUIrvhKbjpGWvxtYv7ZkedWXbzX426xr1d44ttY+3vvWtZQVB2DeGtAjtsMNs6BHT/09R3/4RBPbNvqcfzo3lMl8XHG/Ekq6cY/q8YXNtiMl4e8zPk/oItmCTAFIJJLHN2ErAoYceWpAZEWgDUi0CYg0gm0jsBoJENMlsEA4NBFNgR2MMtAWxBxgEpyLmkNJ0UOmsH7SHJEjaDJVb3c5U7CqrKgROdeedR+DOyCPVBeWQtOomgcHFeMDJJ+teKpjR1sBL/L4tvpj2IhJ/UfnBK2B1ZjC3w1eivT8LFcrfFc8AjGJ6xvbChambhwWSnRKTtX0g3oJXCSBxRgMefPDBvSc96Um9PfbYoyA6wkFAUkrQ8mcRvrQr5RXhNJmCZ4hK+xiSTTFUcNJZiLLfiDqZhv/6K69gJkTpfxIOxuA3CYrp8X/QgPgg8r+3LPuAh/zyIgp905fZxK6PnjWv5c8ifGlP3fqsD5gibc1OUAx7kYi/ePmjfmf3XRtz8bTw1Zyh+02cW4ThbLMqVgUD6ENvfUzK+nDCXBeawB3i3tkhEfaE5JF8zdvhiQgRASlL9fWeOJFp0U4J7oGMGdwDKRPpNbyYKZlBXrMtjCE/zbbl89E/n/yPUCX/m3U0f6sv/+cYsnzWl3Xm89n58/5iXbN9fcN8/KcJIXgBV/ZrWEmhiSyC2g9nbE0Xz2+tkr1/ReAYR18xBxZrXNuyntXEAAocGyYBbeC4QJZNEDOQdUHaAGSj2kK8VLkdEIEZCFihCkNMEjkJzP+lTvqVqfk77xm7lNfZv8vD+GqWzd95zTyzyzbrbOZZ7N8YjUTFJ/HBGDMOQiyEb3OQVQXr/rQbzxfQtyL1Y+526I//pWHrb9J+E7f8Xw1p1TEAk9Ln0kX0h7p+z5jM0wJ5du9rAwvyDUAKSEg9joNMSsgqh5flPCfK8HTTFiDtcmgFC0FCY1kAoSyk6c6y+uaDiYK3K9PGG3rt0RfmKzHHmCtMIIx3geMpuIHRBK54b8VTwq/yRe00ccr/1ZJWJQPoT8664NiDVyyFuv7SuP8S9iJ7Ln4Tz/Ne04OctAEfdrZtqJKlJ6Gs9pHvtNNORWphPD5JcAtE0tLOavxKSQ8+adtjopyd4iTsrTjvvPPK0Gld8uSOywXClE20JXCD30j9Lwt1/zg/+lIfvtyoanmwStJqZgBlipqcO5YL7xpEeHJw+AeY6Pi9ILOgiQMcY5IDSIS4Oneen4Azatdddy0BL6TYhBk0oTbjyHQHbEheDBqMePNJe2r+qaeeWhyaVh6mpqaKpJ+9K3PrWqv/FXU/mMcO/XYvjd/PDJW/nAjSxJ3qGldYxlXPAPrzMYgZ8D+0gSPicmIg3I6BbJAgnYTzhkdKd95xyMQJJTpM2muvvXqPeMQjitPQEpqwYHkwIcjelHzyL1CaqWIsExj5SMaYRO/Kbkf0YhwsZ1544YXlsBV5vY8PXOVJwl8gjAZzHjjAyXd1NOPgjr/SXuCHF9nCiZnOurlK07wRfiXCo8/R6Xg3hNPoVkGEr8DxIWAQY7kfz+a9WtCECQTlNCTVLFEJ4pE2btxYzhewgsB3IFAGcktzMYQFInqpd1t9tRG8Z+x2ocKCnJhQbHsnLEmcepYgmQDpS1mkcSBsZ/NvwHijHycH3I+JN0s7WHBd4MiG0DrkWRNpTTGA/oxupQ2EE+nuwQBeFfiwD6QMpGDvgcuCGUESAD8BZgDhnGWXZ84xDfgLHGvlt5UEnmz5JQzBp4+o5Z6vZAp5HTzYBj+MMVPzN6bqE3AtH2PgKxGbIHiJeo/ovZoNgUuIXiCRcaU3Xx2LlIqdH/Vtr/6A68Xx+6jowz+ofy1J/SY81yIDyPGvj0nfEKoegkeE+wYC21y0B0TuM4IFOQqzobxCPAjNeUUzIAHTeSjP9PR0UXfD8VS0gzgivUjBNCvkQUjNT5PoPG8yheZvz+aTmvU3f6tL/Tkm4/LxXz7quvE5HIUTj2rvBa7OREgGaPefVRPMUX7mEIa3yGk24X85+nhMaBof1E6f8EueRW53RVS3lhlAmaAguq0CO4IRHBoIfHQwgjtB5AYjwAwWBV7q/f/tnT+IHFUcx7N7SaEhghx4EQtPIiFc5IqAJ8GINikU7gwpLBVtbQQL2wM7G7GwFMFgYROCTURsFIMGIYWFnBcVU8hxkSAYglbq5/N2fnsv4+7c3d5MNpudH7x982bevD+/9/v+3u/95s2swdkxlgkKv9aBTsQg99Tr7dahOD8/nywEvyrkYy+djuEpD9AV7U1lqySkqCvKND2MLCfI43IIgEd91mG7na31edh+TXpneF8KEuh67oPcoGM/3E1oWYLenYmWY7pGspNpsw7lHrBs2vkjfX+bpwkfF/V0Gfu0cazGeieuqK0Rn7im19tghEFF4EyQEIIieA2BeQuQHhU0CGnMErV+j9CyA2gqBIPWgeQMKpjc7RbkMkH/wWM4E92Lr5WgH8H1suZzbNMNK8OyovztQGZb8iC4DQFUTXi3FPs+gk869G24FyJeVsqtGdvrO/46PF3ShLKIJY1pybbVTGmNT19nCuD/RJ/eWVlZ+WB1ddVKp26dX8Xf2rlfVdkkXCtMwrQssL3FuwVvAswnTCPACpHKwD0EtU5blJcoQKgACx7BLFAEj7OtAPQ1V8FZpoWFhX2+HSjwnHFVDFoZKoawGsqKIOqLdwoEusfO6BE049fW1vrr9bxeFZH1uQEqb6uKw2D5oYTy+2o87o8J45TGBF59T73vssQ454s71lUe2xrrn9iiWgUweOhucxSaBUVwFiF+A/A8ozA7gyFgKgoFbuQNRZZdRYInSOAOCl43nwpCh5oA1iR3lnb33Kik8jCoONz5qDIKiyLqlA/lEG2WTw1TssqoJ5n51kv4iva8h6l/PuougK+23GJmXJzyuPERmnD+/k8RsDR4GiF7nX6dBQx+GEIAxFSsMmjEKhjGxwBbXBd0uZJwSSF5bieAtD8FkPrANq1yGVRX1HsHY2d7g33ab7+whPwQzHn69z7OvUtek1rg9/hQ9dsqgCrubF2LdWNf+Hh8+CjC9wpZXgZkR8xagCdmGhfyY+VvGbC2cbe0E6Wx2zJHyO/MLV9pTieB3jJQSleJzsH3j/gM+zXPQd3nes49rYN2xpcjFTRWAa1o1117CeFKHjochmnWN807+O4heBXhfB6r4P6YMelELBG0Clpe725UBW8o3ANaMvDX2f4W/P2M4w+Xl5cvFo69feVx2V1V05u7FcoRx96/e+LLxN3YR2AxvAD0CIJ6BsvgJYT0FMqgmymDZBkguJ5rzGcwYnfuitvgjf+pJ+iVy/0Z6D13ieuf4FS8wJ+C/hYN1sxnR+U/4eiL8228Mw60CmBnfKrKlZYHZgirwOOlpaWjmKZnCC8iuE+hDGZUBsUyITmvyBY+g2kdh3yW78Kn9OiO2JleZXCZ408JF1jb9/4qCKZls31r5itse6BpFbw9sGz4rZlVEH6AlFllgECfRpBf4MRJZrYHFXKVgQGK/I5HLBfutbER7AF4Y/uX1vM68lSOrOn/4Nw38OYi5z7npaB10kHJIdvO9sGOeuJ7Tcjq4Uo9pcRW45jtU6kog1mUwUmE/DQnniUsoBDS/vTMQlAreJ80qVZCgD1pOPoxQ5/B9dZ2YQCvj8S3pL6k718QvsWZ9zvpoIE8jIttvHcOtApg7zzctoTMMrhNGXgja9gjAGMJ4T9FeJLjY4DkkECRiiWDM6SACpM3xs04gtnjvMdNkm2RjCNE2jZozhvS40cvFNbOTc6t0ZXviL8mXGb34C9ezyiBvp3pM440eHinBKbBLkxU0QmsrGG7PDno5A7E6AV/HHqY4+PMjieITwCS48TzxEkpEPefx6sTInDeNXMOxigy1UkixrocR74c1J6LsuJ85KOqToeqnNGTZ95YMrYJhdJyB9I10r7ccIX4CkrtB/q8Yd6cdOSxjflffChaC1FvnqU9bogDIQwNFd8Wux0HwjooAJAeLZbv4YMYcywbVAL+Acoxrj9ObPphjmeJ7wvTOr+XPCk5LM7zeiyAq+J0kR/LK0D+F8kbpDe491fCVY7XubaOAvuZfze6HvfkMQrQR6eddpbPuTKe41YBjIfvw2p1PHyqkCyEKqVgAYuLiwfZez8L+OcAnJbDYUA4R/wQYRYw6mx8gONDRThI2q+P+MVbPzrQW2f0nre7HndH3d+EW4SbhD/Jp9nuxzL8as510pvUt8m5DYC+yWO5GwDd/AMpwN7O8APZM/aT/wGRsiNldA1QfQAAAABJRU5ErkJggg==' 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,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAABAKADAAQAAAABAAABAAAAAACU0HdKAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAf1ElEQVR4Ae1dC4xcV3n+d3dmdva93vV6vbZxEucJcR6Ql0PSUpoWVaUKrVoUQEWlAhUhSlVK2whRtRRSCVBDK6VABW0RbYNKEdAqqA8KQhRoQmzHSRxs5+G3d73e92t2d2Zf/b8zc+3NZr3ZOffMnXvmfmc1O8977n++//+/859zz39u3YoWYSECRCCRCNQnstVsNBEgAgYBEgANgQgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGAESQIKVz6YTARIAbYAIJBgBEkCClc+mEwESAG2ACCQYARJAgpXPphMBEgBtgAgkGIFUrbV9eXk5dJPq6uoEjziUFRViZQX/7Yppi92hzo4y0ts3wZkcG1a0Rt1r3m54qM9f1qlxxV01VcEXsFSbBKAZFzzkqh4bRcC4fHYm4x3aAJ/bsJHeaioCyOfz8sMf/lAmJiakvr6+rJ4Tzo7ooa2tTfbt2ycdHR3m+GqRQOC0k3OLcuDEmEzNFGRF/4qmGDyvVW3pcz04la6X3s4muXFXh7Q2Nmhb3JDJ2jNu9D6Qcia/KGfG5mV+YWmjn7/iOxxfr3rJaluyKdXnK37h4AMFpqG+zjxSDfWS0Uejni/TUCdpfb2agKuBoYMWblhFTRBA0Ft/5zvfkfvvv3/DBm/my4985CPy0EMPSTabrQoJBIY2Pb8of/zNY/LFl6ZKfl+GC6hRq/fIu3a2yCd/+WrZs60l0rYEzj8xuyCPHRmR0dlFA30ZLTC/R88LJ9zRlpHObIMsAxzHRQd8Fx29CFuRdFoyDdKeTcmW5rSSaMqQBE4dtM2xGFWpriYIIEDu5MmT5uV9990nc3NzwcebfkZv39DQIA8//LB8+MMflp07d276WJc/RE8Pozx0aly++MKkvKU7I/Nl2j38JKWO89VzM5L9rxPy+XfdKI2lXhROVfFS8pLnh3IyrM7fqVGIjfNC1iWta25hWXqa1VxXd8kuG1HCt/Qks4UlmckvyeBUwTh+u5LPtrZG6VUiyqYbzJkDonYpRtR11RQBpNNpg18ul5PZ2dmysQQBpFIp6erqUjuLxE3WlTE48/B0QUTD0Vmd15xbDkxz3UMu++Gb29LyD2em5YGjw/KWm3ojiwIC+GYLy5LWBsH5LZtghi9LevCiPhCuR1Ew9NBRgIm84OhTGo2NK5GdGp2TXZ2NsruryQwRopClkueoqcuAwXxmWOednp6uJOabrnt+elxkoRCKjGbVaa7L1Mvf7x+Uae3RYNgw6ChLxKdz1jTIHWDVoLhlNKRa0g+Oj8zJgTOTMqLzMr6XmiIAV8oIiMRVfbb1LM3nRAYvSJ1OaF60xDIrW1Qr3q5j2X/tz8n/Hh0yRxcnE8usKOE/D8gAQQGIAEOEQ+em5PjwpUjTR6IjAcTYsOvqVD0vnpbFQr5IApayzmuvdXNTg/zV4+dlNFeoShRgKXosD0NUgIggrcORl0Zy8tPz0xcnBn0jARJALE2sJBQIYGxaZkYu6FhUXwfxaJkyYxKtQycAvzcyL989XIwCinPZZVbEn19EAI6OByZW+yfychQkoB9EM0NxUYzQL0gAoSGsZAVqUZmUTJ3vl8L8bOgo4BaNAv7mwKAMTWlEUYW5gEoiVa264fQYEpydmNe5AR2yaQEx+FJIAHHXlPYwi3pJc3RwoHgJzDIKwAx8m9b1o4mCPPb0YKnVPplqfBUFlTTqoqGTeoXAkKuKaqmmyBtJAogc8jJPqJ5br5cm+wfPS342p1GAXoO2tK681nW7RgGPHBqW/vH5UhRAEihTI5f9Oa5QvjQyK4WlZcX2sj+L1RckgFipY31hzGXNQkGGB84Vw0tL40KaVFZ7qmd0fcG/H9SIwhTLykpH86mIAGgUaxSweOjsWHERmg/USgLwwILNykCNAgYvDMrczGSoKGBeo4C7W1LyIY0CXtJVeuipbFboeQBb5CIiMEvp6qFzOimIy4Sg1riTAAkgcjOxOCGsCJ66tFSMAjCgt4wxURUWAy3rCr1vHihGAXjP4gYBDAPyi8syNJ0vVhhzBiABuNF7xWsxCU/plAwPDUluarx4RSDEXMDdzQ3y4OExOdqviUZafIkC4k5VUImOsuSCDrOwfDnu3EoCqLjrujxB0fwv9J81qcu21oVOycwr6GTVo08OCOYGfIgCzKVLlRVOdvGB9+U8gmP1GBQg6ppUgCVSoJE/gAL54lpIAHHVzDpymShA5wLGR8dkemwkdBRwr14R+IvnJ+Tw6QlztrhHAViHj4m2tMnVLz3jfTmP0rFYyYeR1IL+wwPFFRGg11duFaRCx73UVDZg3MF2Jp/S9sC5s9La2WU2PrGtd0kttVWd6h9/MiCfek2HcSS4gitHsJVr7XFwKDjp7i1Zuaq72fYq6MuqxcTqgiZKoKfG8uiR3IJJ9AExhO6xtQLIHEQAccNzNRAkgNVoePAaUUB9Q0pyk5MyNTokXb07ZXlJQ01YXJmloE71+qaUfPbFSfn142Pyxmu71bmqvxXa2mbAIZGa29feaJberv3e9n2TZo+3a/t3dGYFm68cGZwxzymNKMKQAI7VKmRWd0Ba0FAAOwvFtcRXsrgiFgO54KSYaTqnUUChoAt6kC1oWRa0rp26lPUrGgVg9hrj1zDGbynGxoepQPXwqFJB88MWVGEe+g/Pbbrzzy072wW7AGGoEbYAx3ndxKSgmKKErzGsROsfb28569fHTyNAAMZUrzsX5WdmZGI4XKLQgla2R3fr+eKpGXn8xVEjvSGYCNpR1ilWe9AlLiiritU/RhXmof/wjDZj78Eru5vM3AA+C1NwPDYwWUAmVowLCSDGytlINDNhpxOC5/rPaaLQXKgooKDGf3WmTr70xIDkdAGLiQLibbcbQWP1nbkqokdubc1Is0YBpXlBq7pWH4SoKs6FBBBn7byKbAj9l5AopNmCZg7AMnTFpiE71ei/ek43DTmS7E1DMF7v1HmBRcXSYlrlFRpDXkCcCwkgztp5FdlMqK77IPZrpuB8bkajAPtEIWwa8lrd+PILTw4KtiJPYhQQ8GczNv1MSAREAngVJ4v71yZ0XVjQdGGNAmC1loNXDFW36hj4saE5+e5zOq9gSkK8oNTaALuMpk2b3j9M81UPOHyRcwABunyuBAKIAup0LuD84KDMTk+FjgJu1ijgcxoFYMNLs/IujBNUosER1InFRpY8+nLpFDssB45zYQQQZ+1sVjZ0V5ooNIR0YRic5eAVh2LrsO+P5eU/krxpSLx9drNWsanfkQA2BVO8fxREASPDmig0GS5RCOnCt2ui0GcPDsmAbnNVjAIS5BHxVrVz6UgAziGtUoWlXv98/xldGaj34LONAlT8YNOQbx86X6XG8LRRIUACiArpCp8HUQC2Dpsc00Sh8VFdORfiioBGAXfqbbg++dSwnNItrhgFVFh5VayeBFBF8F2f2gTqujbgvKYLLy7qXWssowDUgwy7c/NL8o3SpiHmaoNrgVlf1REgAVRdBe4EMFGALhGe0dujT44Mh7oigA1E9+lcwEefHZUXNEkGJe7pwu6QTE5NJIAa0zVIQG9xLP2IAkLcUQhRAFJjF3Qp69d00xC8x+IgltpCgARQW/osOmopUWh8SPf/D3FHIVwRuEc3DfnTI2Py3NlJgxSjgNoyGBJAbenzkpMiUWgg/B2FVrTXz2hU8c+aLoycAUYBtWUwJIDa0ufF1tSbRKFZGbugl/IQumNoYFEwF3CnRgGf0U1Dnj6ltyvXwijAAsiYHkICiKliwoplnFQThRAFIFEok2qwXt66uFInvcohX35cIwpd224ShcIKyONjgQAJIBZqqIwQ5tKdJgqNaLrwGb0PAEjBZhoPuwZdpzkCnz8xLU+8OGKENZONlRGbtUaIAAkgQrCjPlUaU4Ir9bI7PyGfuadHN7pIWW93hU1DrkrXyd/95PylTUOibhDP5xwBEoBzSONToVGuLug5OLsob7qhW969t1sO5RalST8rt2AC8DW6ddg/nZ2RHx9jFFAufnH9PQkgrppxIJeZ9lNfR2qALgyWX3lDn2xvSQt2Ay6fAvSWVxoFXJ+pl7/VKABbXidx0xAHaolVFSSAWKnDrTAXnVx3/c3rDrU7Ohrl42/okSdDRAHbdOuwb12Yk++Xtg4rbnvhVm7WFh0CJIDosK7qmYKLgG/TKOCezoxMmy3AyxcJUcBN2Xp55InzMprgTUPKRy6eR5AA4qkX51IFw/7tHVn53dt75em5JclaLO3FDledumnI90bz8t/PJnTrMOfaqV6FJIDqYV+1M//iTb1y39asTGoUgDvulFuwgehtmij01wcuyOBksGlIubXw93FAgAQQBy1EKAP2qOvWve//4O4+edYyCtAqpFm3z94/VZBvP6X5BqYEg4wIG8NThUaABBAaQr8qCKL+n3ltj7x9R4sM6o1AdI6w7DKHJcK6acinnx6Ws2N6YxKtmIuDyoax6geQAKqugmgFgK9jRWBbY0ref1efvJBflkzACmWIgv4em4a8pGsM/u1gsHWYBZOUcU7+1D0CJAD3mMa+xmB3n3tv2Cof2NMuJ/JLoov8yi5IFLpLE4U+rlHA8aFcKeeIQ4GygaziASSAKoJfrVMHUUCjzua/+84+6dc7hNpGAbiV9piuMfja/uKmIQG5VKttPG95CJAAysOrZn4dOOrtV3fJH17XIQf1dmCZ4FphGa0MNg352OFROdI/ZY5kunAZAFb5pySAKiugWqcPogCM43/zrh2iAbwuF7YL37FpSJ3OK/yLbh2GKwRcIlwtrZZ/XhJA+ZjVzBHB7j43X9Epf35jl/wop4uDLKIAzAVg67CHjk7IodMTBp8VSzKpGXA9aQgJwBNFVUpMXLpDNPDAHX2S1ZuDLpXel3s+bBrSo9b0Fd00ZKG0aUi5dfD30SNAAoge81idsXj9XuT6vjb51C1b5QlNFLKJArBpyPW6acgjJ6bkwImxWLWRwlweARLA5bFJ0DfFsf/bbuuTvqaULGhIb3FVUDcNXZHduqroS48PyKxeGUDhUCDeZkQCiLd+IpGuGAWsyJVbm+UTt22Tn+jiHrsoQORK3TTky6en5fEXgk1DImkCT2KJAAnAErjaO6zY57/11u2yV3MF5peWraIApAtf21gvX3hiwGwdhrUGuDLAEk8ESADx1EvkUmE1MCYE+zqz8tG7euXA7JL11mG9umnIN87Pyg+ODOtNSm0GE5E3P7EnJAEkVvXrNbzorL90c6+8RdOFJ2zThbXLf4POJXziB2flscND0qxXFxgFrId39T8jAVRfB7GRAFEAVvF1tWTkg5oodFijAJtNQxDxa7awFJRAvvXCuEzMLuh7ZAvGpqkUpIQACYCm8DIEdE2fef8mTRf+jR3NMqSz+TbpwujxM3p3onr1+sP90yYCsEg6fJlsfOMeARKAe0y9rjGIAjo0hP/A3Tvk2PySNKL3tmgVLgu2aihwaHhOhnTnIEYBFiBW+BASQIUB9rH6IFFo33Xd8p4r2uS0kkDGdi4PwwoNB54+Ny2L+swoIF4WQQKIlz5iIQ18HXMBzekGed++HXJa7wqStvRcDAXadAzx7Ni8DIzPMQqIhYYvCUECuIQFX61CIEgUuuvaLvkjTRd+yjJdGFVi+JDRxyGNAjAxyCuDQCUehQQQDz3EUgpEASnt+d95h6YLa7KPbbowooBmjQKOTOTlzOisNOhWxNw/MB4qJwHEQw+xlCKIAm65slM+fuMW+ZFeFsSEoE0BCbTosU/1z8isbkTKBUI2KLo/hgTgHtOaqhFRAIzkHXfuKF7c1/c2BUdlted/abogxy/kSnMBdnXZnJ/HrI8ACWB9XPhpCYEgCkC68Kdv6pb/w+KgEFFAu5LAwYEZmdY5BXNZkEhXFQESQFXh9+PkwXj9AY0CdmvOv226MPp77Ds4oNmGL2oUYC43MgioqhGQAKoKvx8nD9KFr9B04Y/h7sKW6cJoLeYCOjRD8EmNAsZzMV0ibDfN4Ycy10hJAlgDCN9eDoGiV7z11j65vjUted32y8ZP0OGr/8u4TgQeOz8jVpVcTkRHn2PhkpPARAGK+2QnCcCR0dR6NVgHhKHAzi1Z+bO7tst+jQKaLOcCgjsMH9RhwPBUXlLmsmAMECx5PdYqmLlOG4YLmqF14fC0ti3OhQQQZ+3ETraiMf/C3l752a5GmQmxqAfcMafHFxOF4rFEOFjsOLew5AR5wydOQgkn4qxbCQlgXVj44XoIBFFAT1tGfl/ThQ9h05DAa9Y7YIPPEAW061jgmZE5GdQFQnFJFMLdkyfncYUCEc8GDdjEV6BLRDdxLiSAOGsnlrIVDfq+vdvkHbvs7y5smoaq1Mme1jsKYStxSy5xglLg7OO6d8GM3isxuPwZtvIsJjxiXOItXZWAa2hoKF6iqtL543xaOCkWB7VnU/Lbd2w3dxdutPRcXBFo1SXCh0fz0q+3GN/0XEDInnk1vqgKzo8moPc/qUuVUYo0Z15a/UO9iGrSJAAr/KpyECa56nUTi3w+L5OTk0aG5eVlTWeN/oGTw9FCW2IFkAzShe+9oUfed2Vb6e7Cdi4DR8lqN/SUbhoyr5uPXG5eEb8LzusSE0gN58e5nxuYlkldoIQbnuJ8YQp0h94/g7GEFjt0wkiwuWMZAazBCb0/yunTp80z3oMUonwEMmQymkOnqbiuwlHTIAf/YMwwcOz19x6dCzhn0oXtKkYU0KTj5Ocn83JyeNZcNgPvrS5B7zw6Uyh+rN/jN/hZ2Y/ScejtMdmHkP/4cE72n5mQYa3fhfMDH8jXpPhkYh4BpFYDzdfaE8zPy7333itf//rXzeve3l5VJswswqKnS6nhHD2qJNTdKDndojtuJeiN77i6Wz6454J8Te8FcK3eE6BggZUZCmive1DTha/obpLG0iaicCQUVInLacdHZmVInTT4vPht+f9xPG6BhhWNeb0SoQGecXwXzm+k0ROgTRgmocB6wspsKqrAPxIAlKNdDMJ89PJbt26VlpYW6e/vlwcffFBGRkYuhZ4VUMC6VdZnpG5mQPL7fkek50YZXlwoxqnr/rg6H8KgEQVk1DHfe/dO+dypY3KdZeAMB2nUes5ob4wlwjfvbpdldUwTm5eaFzjQTH7REELYViOqwP6HDfoipV4AkoEcTopWhKFMR1PaSXWVrCTxBADnX1paknQ6Ldu2bZPGxkbzHuH33r17DSlEHgGoYdbLgpyQNjmgxIRNORcraQWWdQdDk1s1XfhPbuiUh46Nyxt1L0HcLbjcYnpMHS/v1yXCV2xt0t4zbSbl4KhBwcvgcuGqj4Ovy34OpITzuyyILlr03gjYVxHFhawu5VtdV6IJIHB+OH1PT4/A6eHs+BzPhUJpzLkasUhea9+0kpeFNGLT8BNSlRQZUQCI4AHdNOSh5ydUbjtvwlEa+csFnYR7cTAnt1/V+Qqx8ZugeruzvKJK5x+AsLQ/EayVSCuhQc44E0BiJwED529qahKM8wPnd24RthXG2WpWtSmIAvbuape/vHWr/DhEujAWB21RFth/PidjmJDTYUFcHX0VBC97CYKC3L1tjS/7PK5vEkkAcP7FxUVpbW01YX9KB4HRh/lxNYny5UIUgPJrenfhrE4EIpnGlr/Ud2RKZ+efC+4lUL44VTsCvT8WNPWp87eVJgBtcYiqEYkkAIz5Ozs7zYQfLrnR+cOZG6IAcMCenhZ5+PU98niIdGFEAUgXfkrvJYBEoeKmIUWCCSdl5Y/GPEZWI5gr9UoGig9SJ44AMNu/ZcsW6e7uvjjWr7xpJOcMiAJu0/HvnF66tDUu9KS4h8AzukTY3EvAOp6IDncjs7LXHnX+Jp0ABCHGvfcHOrY6ig5Zh2dCT4/LfCAA9voOgdWq4ADAFHcX/tBt2+RgiLkAc0VAJz+fGZnXRCG9o1DM5wLQ9oIuhtrZ2Si7thR7f3zmQ6l5Aghm9KEMzPS3t7fT+StmmUWrx92Ff747K1N6LR9jepuC8BmXqHBHIeTnW1Zjc+qyjoGjY9zf3ZKW63tbzbE+hP5BI2uaAOD8wQIfzPRj0o89f6B6989wBkwI9rY3yu/ppiHPzIXbQLRFo4Cfjl+6l4B7icPViPai59+iC35u3tl+cRlxXMlqvdbWLAEEl/kww799+3bB5T46/3om4Paz4O7CP/e6bXL/dr27cCFEFKBdaZNaKO4lgGQd6DQOJRADzr+jo1Fu0UugWKrsy7h/NYY1SwCY6ccCHzg/npPs/FG6DZwDWJu7C2ui0LH8slinC6ulIlHo2FRBTg7lTKLQauON+nXg+MHeBTf0tsjeHW1F51dhgu+jlivM+WqSABD2Nzc3mwU+WOLrt/PbjyiDI9uiZABYY8kT3vy6Hvmt17TIgG6wYX1HIa2uXRfWH9EcgXldH4A19kG7whj+Zo9FUwAfenc4PoY4fTrEuWN3h+zuunS5L2qINyv/q/2uppYCo9dH6ejoMCG/r9f4sYymYWVJWhqWpSnVIJ1qdGt3qdtMOIxNO09pItF1LRnZ0lo01qI5v5pZhPu+6DAr0qjX899/zy75ylePyVZ9jc0/Fsr0XkwiLtRp0pG2Bdl6KPhffGXeuvm3ukKVEfmX6DiQiImvkNvf15qRvo6sdJaSfEAKAUG4ESL6WmqKAK655hqDIFb5IYsPkcBmHCV62Dc+oyEATf/J1S3KXFOnzNXBeWGSgZWq5WHFzKsV/PzgrLz3y2+UtubiMCgqPILz3H1Nl/znr+6Rd/7PGZmY0qxGm8sCOtZ++42tZoltXl9XKtSGQ6NoUqjmJdRpWnJKk5IapKs5LZ36yOrt0oOCn1ZKjuAcUTxr7kbQ7ChOV5lzoAkwuFwuJ48++qhJ5fW197+EEKbTlqWgzj9Tv0Wfsxr6qkdrW+t19WK2tb1kgQERBORQpAnsbN+qTr/vtbvkvjuu140pSotTLv3s0qkieIW7Ah84OSEnNKe/OKG3cSgPMWGZzbqo5irNDrx6e+tF+nMtLpJ2EFzgGVELHlk9L3p9rERcXYzjr/7A89c1QQBrdeBrz7+2HcX3wU0qLiXGwCSDHnb9Y4qfrjbdahpuQNAbyRrn70BEtdDbr4dxTRGA74a2noLCfmYCPLXe1WQQtk6b4+FE6PM3Q1xr64/KAUsimhCq2nitxaBS72uKACoFEuslArWKQE1eBqxVZbFdRMA1AiQA14iyPiLgEQIkAI+URVGJgGsESACuEWV9RMAjBEgAHimLohIB1wiQAFwjyvqIgEcIkAA8UhZFJQKuESABuEaU9REBjxAgAXikLIpKBFwjQAJwjSjrIwIeIUAC8EhZFJUIuEaABOAaUdZHBDxCgATgkbIoKhFwjQAJwDWirI8IeIQACcAjZVFUIuAaARKAa0RZHxHwCAESgEfKoqhEwDUCJADXiLI+IuARAiQAj5RFUYmAawRIAK4RZX1EwCMESAAeKYuiEgHXCJAAXCPK+oiARwiQADxSFkUlAq4RIAG4RpT1EQGPECABeKQsikoEXCNAAnCNKOsjAh4hQALwSFkUlQi4RoAE4BpR1kcEPEKABOCRsigqEXCNAAnANaKsjwh4hAAJwCNlUVQi4BoBEoBrRFkfEfAIARKAR8qiqETANQIkANeIsj4i4BECJACPlEVRiYBrBEgArhFlfUTAIwRIAB4pi6ISAdcIkABcI8r6iIBHCJAAPFIWRSUCrhEgAbhGlPURAY8QIAF4pCyKSgRcI0ACcI0o6yMCHiFAAvBIWRSVCLhGgATgGlHWRwQ8QoAE4JGyKCoRcI0ACcA1oqyPCHiEAAnAI2VRVCLgGgESgGtEWR8R8AgBEoBHyqKoRMA1AiQA14iyPiLgEQIkAI+URVGJgGsESACuEWV9RMAjBEgAHimLohIB1wiQAFwjyvqIgEcIkAA8UhZFJQKuESABuEaU9REBjxAgAXikLIpKBFwjQAJwjSjrIwIeIUAC8EhZFJUIuEaABOAaUdZHBDxCgATgkbIoKhFwjQAJwDWirI8IeIQACcAjZVFUIuAaARKAa0RZHxHwCAESgEfKoqhEwDUCJADXiLI+IuARAiQAj5RFUYmAawRIAK4RZX1EwCMESAAeKYuiEgHXCJAAXCPK+oiARwiQADxSFkUlAq4RIAG4RpT1EQGPECABeKQsikoEXCPw/wMnrnSYEqYJAAAAAElFTkSuQmCC' 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 b3e84b3f2..6e8518b6b 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -3,12 +3,26 @@ import { PackageDataEntry, } 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 { 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, CB } from '@start9labs/start-sdk' -import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' +import { GetPackagesRes } from '@start9labs/marketplace' + +const mockBlake3Commitment: T.Blake3Commitment = { + hash: 'fakehash', + size: 0, +} + +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: T.ServerStatus = { @@ -23,6 +37,7 @@ export module Mock { headline: 'Our biggest release ever.', 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', @@ -37,17 +52,44 @@ 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: T.Manifest = { id: 'bitcoind', title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', + satisfies: [], + canMigrateTo: '!', + canMigrateFrom: '*', gitHash: 'abcdefgh', description: { short: 'A Bitcoin full node by Bitcoin Core.', @@ -90,7 +132,10 @@ export module Mock { 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.', @@ -116,11 +161,13 @@ export module Mock { bitcoind: { description: 'LND needs bitcoin to live.', optional: true, + s9pk: '', }, 'btc-rpc-proxy': { description: 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', optional: true, + s9pk: '', }, }, hasConfig: true, @@ -143,7 +190,10 @@ export module Mock { export const MockManifestBitcoinProxy: T.Manifest = { id: 'btc-rpc-proxy', title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', + satisfies: [], + canMigrateTo: '!', + canMigrateFrom: '*', gitHash: 'lmnopqrx', description: { short: 'A super charger for your Bitcoin node.', @@ -168,6 +218,7 @@ export module Mock { bitcoind: { description: 'Bitcoin Proxy requires a Bitcoin node.', optional: false, + s9pk: '', }, }, hasConfig: false, @@ -187,150 +238,510 @@ export module Mock { }, } - export const BitcoinDep: DependencyMetadata = { + export const BitcoinDep: T.DependencyMetadata = { title: 'Bitcoin Core', icon: BTC_ICON, optional: false, - hidden: true, + description: 'Needed to run', } - export const ProxyDep: DependencyMetadata = { + export const ProxyDep: T.DependencyMetadata = { title: 'Bitcoin Proxy', icon: PROXY_ICON, optional: true, - hidden: false, + 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', + '=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!', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, - '0.20.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.20.0', + }, + lnd: { + '=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', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, - '0.21.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.21.0', - releaseNotes: - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', + '=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', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, - latest: { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - releaseNotes: - '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
', + }, + 'btc-rpc-proxy': { + '=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'], + otherVersions: { + '0.3.2.7:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, }, - lnd: { - '0.11.0': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.0', - releaseNotes: 'release notes for LND 0.11.0', + } + + 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(), + }, }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + '#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(), + }, }, - publishedAt: new Date().toISOString(), }, - '0.11.1': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.1', - releaseNotes: 'release notes for LND 0.11.1', + categories: ['bitcoin', 'featured'], + otherVersions: { + '26.1.0:0.1.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + '#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(), + }, }, - publishedAt: 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'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + 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', }, - publishedAt: new Date(new Date().valueOf() + 10).toISOString(), }, }, 'btc-rpc-proxy': { - latest: { - icon: PROXY_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestBitcoinProxy, - categories: ['bitcoin'], - versions: ['0.2.2'], - dependencyMetadata: { - bitcoind: BitcoinDep, + 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.', }, - publishedAt: new Date().toISOString(), }, }, } - export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = - Object.values(Mock.MarketplacePkgs).map(service => service['latest']) - export const Notifications: ServerNotifications = [ { id: 1, @@ -529,6 +940,7 @@ export module Mock { current: 'b7b1a9cef4284f00af9e9dda6e676177', sessions: { '9513226517c54ddd8107d6d7b9d8aed7': { + loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2021-07-14T20:49:17.774Z', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', metadata: { @@ -536,6 +948,7 @@ export module Mock { }, }, b7b1a9cef4284f00af9e9dda6e676177: { + 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', @@ -668,13 +1081,13 @@ export module Mock { packageBackups: { bitcoind: { title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, @@ -1287,6 +1700,7 @@ export module Mock { state: 'installed', manifest: MockManifestBitcoind, }, + dataVersion: MockManifestBitcoind.version, icon: '/assets/img/service-icons/bitcoind.svg', lastBackup: null, status: { @@ -1302,7 +1716,6 @@ export module Mock { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: @@ -1320,7 +1733,6 @@ export module Mock { rpc: { id: 'rpc', hasPrimary: false, - disabled: false, masked: false, name: 'RPC', description: @@ -1338,7 +1750,6 @@ export module Mock { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -1466,6 +1877,7 @@ export module Mock { state: 'installed', manifest: MockManifestBitcoinProxy, }, + dataVersion: MockManifestBitcoinProxy.version, icon: '/assets/img/service-icons/btc-rpc-proxy.png', lastBackup: null, status: { @@ -1479,7 +1891,6 @@ export module Mock { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: 'A launchable web app for Bitcoin Proxy', @@ -1499,8 +1910,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: '', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1516,6 +1926,7 @@ export module Mock { state: 'installed', manifest: MockManifestLnd, }, + dataVersion: MockManifestLnd.version, icon: '/assets/img/service-icons/lnd.png', lastBackup: null, status: { @@ -1529,7 +1940,6 @@ export module Mock { grpc: { id: 'grpc', hasPrimary: false, - disabled: false, masked: false, name: 'GRPC', description: @@ -1547,7 +1957,6 @@ export module Mock { lndconnect: { id: 'lndconnect', hasPrimary: false, - disabled: false, masked: true, name: 'LND Connect', description: @@ -1565,7 +1974,6 @@ export module Mock { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -1586,8 +1994,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1595,8 +2002,7 @@ export module Mock { title: Mock.MockManifestBitcoinProxy.title, icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'exists', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', configSatisfied: false, }, }, 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 cac9d7a63..a7d39dd1f 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,5 +1,4 @@ import { Dump } from 'patch-db-client' -import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' @@ -34,6 +33,7 @@ export module RR { export type LoginReq = { password: string metadata: SessionMetadata + ephemeral?: boolean } // auth.login - unauthed export type loginRes = null @@ -223,19 +223,14 @@ export module RR { export type GetPackageMetricsReq = { id: string } // package.metrics export type GetPackageMetricsRes = Metric - export type InstallPackageReq = { - id: string - versionSpec?: string - versionPriority?: 'min' | 'max' - registry: string - } // package.install + export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null export type GetPackageConfigReq = { id: string } // package.config.get export type GetPackageConfigRes = { spec: CT.InputSpec; config: object } export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry - export type DrySetPackageConfigRes = Breakages + export type DrySetPackageConfigRes = T.PackageId[] export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set export type SetPackageConfigRes = null @@ -287,26 +282,13 @@ export module RR { progress: string // guid } - // marketplace + // registry - export type GetMarketplaceInfoReq = { serverId: string } - export type GetMarketplaceInfoRes = StoreInfo + /** 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 CheckOSUpdateReq = { serverId: string } export type CheckOSUpdateRes = OSUpdate - - export type GetMarketplacePackagesReq = { - ids?: { id: string; version: string }[] - // iff !ids - category?: string - query?: string - page?: number - perPage?: number - } - export type GetMarketplacePackagesRes = MarketplacePkg[] - - export type GetReleaseNotesReq = { id: string } - export type GetReleaseNotesRes = { [version: string]: string } } export interface OSUpdate { @@ -372,6 +354,7 @@ export interface Metric { } export interface Session { + loggedIn: string lastActive: string userAgent: string metadata: SessionMetadata @@ -554,7 +537,7 @@ export interface DependencyErrorConfigUnsatisfied { export interface DependencyErrorHealthChecksFailed { type: 'healthChecksFailed' - check: T.HealthCheckResult + check: T.NamedHealthCheckResult } export interface DependencyErrorTransitive { 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 a5bce8c62..31e3ac86e 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,22 +1,35 @@ import { Observable } from 'rxjs' import { RR } from './api.types' +import { RPCOptions } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' export abstract class ApiService { // http - // for getting static files: ex icons, instructions, licenses - abstract getStatic(url: string): Promise - // for sideloading packages - abstract uploadPackage(guid: string, body: Blob): Promise + abstract uploadPackage(guid: string, body: Blob): Promise - abstract uploadFile(body: Blob): Promise + // for getting static files: ex icons, instructions, licenses + abstract getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + + abstract getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise // websocket abstract openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config?: RR.WebsocketConfig, ): Observable // state @@ -120,14 +133,23 @@ export abstract class ApiService { // marketplace URLs - abstract marketplaceProxy( - path: string, - params: Record, - url: string, + abstract registryRequest( + registryUrl: string, + options: RPCOptions, ): 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 abstract getNotifications( 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 cef2c13ff..48cf6edd3 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 @@ -18,6 +18,15 @@ import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' 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 { @@ -32,32 +41,42 @@ export class LiveApiService extends ApiService { ; (window as any).rpcClient = this } - // for getting static files: ex icons, instructions, licenses + // for sideloading packages - async getStatic(url: string): Promise { - return this.httpRequest({ - method: Method.GET, - url, - responseType: 'text', + async uploadPackage(guid: string, body: Blob): Promise { + await this.httpRequest({ + method: Method.POST, + body, + url: `/rest/rpc/${guid}`, }) } - // for sideloading packages + // for getting static files: ex. instructions, licenses + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + const encodedUrl = encodeURIComponent(pkg.s9pk.url) - async uploadPackage(guid: string, body: Blob): Promise { return this.httpRequest({ - method: Method.POST, - body, - url: `/rest/rpc/${guid}`, + method: Method.GET, + url: `/s9pk/proxy/${encodedUrl}/${path}`, + params: { + rootSighash: pkg.s9pk.commitment.rootSighash, + rootMaxsize: pkg.s9pk.commitment.rootMaxsize, + }, responseType: 'text', }) } - async uploadFile(body: Blob): Promise { + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { return this.httpRequest({ - method: Method.POST, - body, - url: `/rest/upload`, + method: Method.GET, + url: `/s9pk/installed/${id}.s9pk/${path}`, responseType: 'text', }) } @@ -66,7 +85,7 @@ export class LiveApiService extends ApiService { openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config: RR.WebsocketConfig = {}, ): Observable { const { location } = this.document.defaultView! const protocol = location.protocol === 'http:' ? 'ws' : 'wss' @@ -257,24 +276,61 @@ export class LiveApiService extends ApiService { // 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 checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { - return this.marketplaceProxy( - '/eos/v0/latest', - qp, - this.config.marketplace.start9, - ) + const { serverId } = qp + + 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 @@ -504,6 +560,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 21491dc52..a40f29a0a 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,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared' +import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { Operation, @@ -30,8 +30,12 @@ import { } from 'rxjs' import { mockPatchData } from './mock-patch' import { AuthService } from '../auth.service' -import { StoreInfo } from '@start9labs/marketplace' import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' const PROGRESS: T.FullProgress = { overall: { @@ -48,10 +52,7 @@ const PROGRESS: T.FullProgress = { }, { name: 'Validating', - progress: { - done: 0, - total: 40, - }, + progress: null, }, { name: 'Installing', @@ -80,21 +81,31 @@ export class MockApiService extends ApiService { .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, + config: RR.WebsocketConfig = {}, ): Observable { if (guid === 'db-guid') { return this.mockWsSource$.pipe( @@ -113,6 +124,11 @@ export class MockApiService extends ApiService { return from(this.initProgress()).pipe( startWith(PROGRESS), ) as Observable + } else if (guid === 'sideload-progress-guid') { + config.openObserver?.next(new Event('')) + return from(this.initProgress()).pipe( + startWith(PROGRESS), + ) as Observable } else { throw new Error('invalid guid type') } @@ -136,7 +152,7 @@ export class MockApiService extends ApiService { this.stateIndex++ - return this.stateIndex === 1 ? 'initializing' : 'running' + return this.stateIndex === 1 ? 'running' : 'running' } // db @@ -448,34 +464,13 @@ export class MockApiService extends ApiService { // marketplace URLs - async marketplaceProxy( - path: string, - params: Record, - url: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { await pauseFor(2000) - 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 - } + return Error('do not call directly') } async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { @@ -483,6 +478,29 @@ export class MockApiService extends ApiService { return Mock.MarketplaceEos } + async getRegistryInfo(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryInfo + } + + 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 getRegistryPackages(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryPackages + } + // notification async getNotifications( @@ -742,11 +760,11 @@ export class MockApiService extends ApiService { ...Mock.LocalPkgs[params.id], stateInfo: { // if installing - // state: PackageState.Installing, + state: 'installing', // if updating - state: 'updating', - manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, + // state: 'updating', + // manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, // both installingInfo: { @@ -776,7 +794,7 @@ export class MockApiService extends ApiService { params: RR.DrySetPackageConfigReq, ): Promise { await pauseFor(2000) - return {} + return [] } async setPackageConfig( @@ -1065,16 +1083,11 @@ export class MockApiService extends ApiService { async sideloadPackage(): Promise { await pauseFor(2000) return { - upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated - progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated + upload: 'sideload-upload-guid', // no significance, randomly generated + progress: 'sideload-progress-guid', // no significance, randomly generated } } - async uploadFile(body: Blob): Promise { - await pauseFor(2000) - return 'returnedhash' - } - private async initProgress(): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) @@ -1129,11 +1142,7 @@ export class MockApiService extends ApiService { 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 - ) { + if (!phase.progress || phase.progress === true || !phase.progress.total) { await pauseFor(2000) const patches: Operation[] = [ 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 2fa7982f4..d69a64afa 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -73,7 +73,7 @@ export const mockPatchData: DataModel = { platform: 'x86_64-nonfree', zram: true, governor: 'performance', - smtp: 'todo', + smtp: null, wifi: { interface: 'wlan0', ssids: [], @@ -87,9 +87,10 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestBitcoind, - version: '0.20.0', + version: '0.20.0:0', }, }, + dataVersion: '0.20.0:0', icon: '/assets/img/service-icons/bitcoind.svg', lastBackup: null, status: { @@ -131,7 +132,6 @@ export const mockPatchData: DataModel = { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: @@ -149,7 +149,6 @@ export const mockPatchData: DataModel = { rpc: { id: 'rpc', hasPrimary: false, - disabled: false, masked: false, name: 'RPC', description: @@ -167,7 +166,6 @@ export const mockPatchData: DataModel = { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -294,9 +292,10 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestLnd, - version: '0.11.0', + version: '0.11.0:0.0.1', }, }, + dataVersion: '0.11.0:0.0.1', icon: '/assets/img/service-icons/lnd.png', lastBackup: null, status: { @@ -310,7 +309,6 @@ export const mockPatchData: DataModel = { grpc: { id: 'grpc', hasPrimary: false, - disabled: false, masked: false, name: 'GRPC', description: @@ -328,7 +326,6 @@ export const mockPatchData: DataModel = { lndconnect: { id: 'lndconnect', hasPrimary: false, - disabled: false, masked: true, name: 'LND Connect', description: @@ -346,7 +343,6 @@ export const mockPatchData: DataModel = { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -367,8 +363,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Core', icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -376,8 +371,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Proxy', icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'running', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', healthChecks: [], configSatisfied: false, }, diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index 42116bdd8..69fafe815 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -59,50 +59,48 @@ export class ConfigService { /** ${scheme}://${username}@${host}:${externalPort}${suffix} */ launchableAddress( interfaces: PackageDataEntry['serviceInterfaces'], - host: T.Host, + hosts: PackageDataEntry['hosts'], ): string { - const ui = Object.values(interfaces).find(i => i.type === 'ui') + const ui = Object.values(interfaces).find( + i => + i.type === 'ui' && + (i.addressInfo.scheme === 'http' || + i.addressInfo.sslScheme === 'https'), + ) // TODO: select if multiple if (!ui) return '' + const hostnameInfo = + hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort] + + if (!hostnameInfo) return '' + const addressInfo = ui.addressInfo - const scheme = this.isHttps() ? 'https' : 'http' + const scheme = this.isHttps() + ? ui.addressInfo.sslScheme === 'https' + ? 'https' + : 'http' + : ui.addressInfo.scheme === 'http' + ? 'http' + : 'https' const username = addressInfo.username ? addressInfo.username + '@' : '' const suffix = addressInfo.suffix || '' const url = new URL(`${scheme}://${username}placeholder${suffix}`) - if (host.kind === 'multi') { - const onionHostname = host.addresses.find(h => h.kind === 'onion') - ?.address as T.OnionHostname | undefined - - if (!onionHostname) - throw new Error('Expecting that there is an onion hostname') - - if (this.isTor() && onionHostname) { - url.hostname = onionHostname.value - } - // TODO Handle single - // else { - // const ipHostname = host.addresses.find(h => h.kind === 'ip') - // ?.hostname as T.ExportedIpHostname - - // if (!ipHostname) return '' + const onionHostname = hostnameInfo.find(h => h.kind === 'onion') + ?.hostname as T.OnionHostname | undefined - // url.hostname = this.hostname - // url.port = String(ipHostname.sslPort || ipHostname.port) - // } + if (this.isTor() && onionHostname) { + url.hostname = onionHostname.value } else { - throw new Error('unimplemented') - // const hostname = {} as T.ExportedHostnameInfo // host.hostname + const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as + | T.IpHostname + | undefined - // if (!hostname) return '' + if (!ipHostname) return '' - // if (this.isTor() && hostname.kind === 'onion') { - // url.hostname = (hostname.hostname as T.ExportedOnionHostname).value - // } else { - // url.hostname = this.hostname - // url.port = String(hostname.hostname.sslPort || hostname.hostname.port) - // } + url.hostname = this.hostname + url.port = String(ipHostname.sslPort || ipHostname.port) } return url.href 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 5abee0d6b..a46a5123e 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -1,5 +1,5 @@ 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 { @@ -39,7 +39,7 @@ export class DepErrorService { ) constructor( - private readonly emver: Emver, + private readonly exver: Exver, private readonly patch: PatchDB, ) {} @@ -87,11 +87,17 @@ export class DepErrorService { const depManifest = dep.stateInfo.manifest // incorrect version - if (!this.emver.satisfies(depManifest.version, currentDep.versionSpec)) { - return { - type: 'incorrectVersion', - expected: currentDep.versionSpec, - 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, + } } } diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/eos.service.ts index 81b97bf11..c61c667e3 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/eos.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' import { BehaviorSubject, combineLatest } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' import { OSUpdate } from 'src/app/services/api/api.types' @@ -7,6 +6,7 @@ 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 { Exver } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -47,15 +47,15 @@ export class EOSService { constructor( private readonly api: ApiService, - private readonly emver: Emver, private readonly patch: PatchDB, + private readonly exver: Exver, ) {} async loadEos(): Promise { const { version, id } = await getServerInfo(this.patch) this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) const updateAvailable = - this.emver.compare(this.osUpdate.version, version) === 1 + this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater' this.updateAvailable$.next(updateAvailable) } } diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index 3cd0ee591..779fb1f53 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -122,11 +122,6 @@ export class FormService { return this.getListItem(spec, entry) }) return this.formBuilder.array(mapped, listValidators(spec)) - case 'file': - return this.formBuilder.control( - currentValue || null, - fileValidators(spec), - ) case 'union': const currentSelection = currentValue?.selection const isValid = !!spec.variants[currentSelection] @@ -259,16 +254,6 @@ function listValidators(spec: CT.ValueSpecList): ValidatorFn[] { return validators } -function fileValidators(spec: CT.ValueSpecFile): ValidatorFn[] { - const validators: ValidatorFn[] = [] - - if (spec.required) { - validators.push(Validators.required) - } - - return validators -} - export function numberInRange( min: number | null, max: number | null, diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 40d2247e7..bc2dcc7ab 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, @@ -19,11 +19,7 @@ import { } from 'rxjs' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - DataModel, - UIMarketplaceData, - UIStore, -} from 'src/app/services/patch-db/data-model' +import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { catchError, @@ -37,15 +33,16 @@ 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', 'knownHosts') .pipe( - map((hosts: UIMarketplaceData['knownHosts']) => { + map(hosts => { const { start9, community } = this.config.marketplace let arr = [ toStoreIdentity(start9, hosts[start9]), @@ -93,11 +90,9 @@ export class MarketplaceService implements AbstractMarketplaceService { mergeMap(({ url, name }) => this.fetchStore$(url).pipe( tap(data => { - if (data?.info) this.updateStoreName(url, name, data.info.name) - }), - map(data => { - return [url, data] + if (data?.info.name) this.updateStoreName(url, name, data.info.name) }), + map(data => [url, data]), startWith<[string, StoreData | null]>([url, null]), ), ), @@ -148,6 +143,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 { @@ -170,28 +166,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.selectedUrl + return this.selectedHost$.pipe( + switchMap(selected => + this.marketplace$.pipe( + switchMap(m => { + const url = registryUrl || selected.url - if (version !== '*' || !uiMarketplace.knownHosts[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) + }), + ), + ), ) } @@ -210,56 +207,22 @@ export class MarketplaceService implements AbstractMarketplaceService { ): Promise { const params: RR.InstallPackageReq = { id, - versionSpec: `=${version}`, + version, registry: url, } await this.api.installPackage(params) } - fetchInfo$(url: string): Observable { - return this.patch.watch$('serverInfo').pipe( - take(1), - switchMap(serverInfo => { - const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id } - return this.api.marketplaceProxy( - '/package/v0/info', - qp, - 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, - ), - ) - }), - ) + fetchInfo$(url: string): Observable { + return from(this.api.getRegistryInfo(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 { @@ -273,33 +236,62 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - private fetchPackages$( - url: string, - params: Omit = {}, - ): Observable { - const qp: RR.GetMarketplacePackagesReq = { - ...params, - page: 1, - perPage: 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, + ), + ), ) } 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 52728a06a..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 @@ -49,6 +49,10 @@ export type PackageDataEntry = stateInfo: T } +export type AllPackageData = NonNullable< + T.AllPackageData & Record> +> + export type StateInfo = InstalledState | InstallingState | UpdatingState export type InstalledState = { 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 index 0b80370d0..5d1f1ed51 100644 --- 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 @@ -5,6 +5,7 @@ import { bufferTime, catchError, filter, + skip, startWith, switchMap, take, @@ -32,7 +33,7 @@ export class PatchDbSource extends Observable[]> { private readonly stream$ = inject(AuthService).isVerified$.pipe( switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)), switchMap(({ dump, guid }) => - this.api.openWebsocket$(guid, {}).pipe( + this.api.openWebsocket$(guid).pipe( bufferTime(250), filter(revisions => !!revisions.length), startWith([dump]), @@ -41,8 +42,8 @@ export class PatchDbSource extends Observable[]> { catchError((_, original$) => { this.state.retrigger() - // @TODO this is returning right away, but we need to wait until state emits again from the retrigger() above. return this.state.pipe( + skip(1), // skipping previous value stored due to shareReplay filter(current => current === 'running'), take(1), switchMap(() => original$), diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts index 33569a751..9eb8caf0a 100644 --- a/web/projects/ui/src/app/services/state.service.ts +++ b/web/projects/ui/src/app/services/state.service.ts @@ -111,7 +111,7 @@ export class StateService extends Observable { ), ), ) - .subscribe() // @TODO shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe? + .subscribe() constructor() { super(subscriber => this.stream$.subscribe(subscriber)) 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 9c36036f3..7c3475fc0 100644 --- a/web/projects/ui/src/app/services/ui-launcher.service.ts +++ b/web/projects/ui/src/app/services/ui-launcher.service.ts @@ -13,11 +13,12 @@ export class UiLauncherService { private readonly config: ConfigService, ) {} - launch(interfaces: PackageDataEntry['serviceInterfaces']): void { - // TODO @Matt - const host = {} as any + launch( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { this.windowRef.open( - this.config.launchableAddress(interfaces, host), + this.config.launchableAddress(interfaces, hosts), '_blank', 'noreferrer', ) diff --git a/web/projects/ui/src/app/util/dry-update.ts b/web/projects/ui/src/app/util/dry-update.ts index 2d4d1aa10..e9386df3c 100644 --- a/web/projects/ui/src/app/util/dry-update.ts +++ b/web/projects/ui/src/app/util/dry-update.ts @@ -1,18 +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['packageData'], - emver: Emver, + exver: Exver, ): string[] { return Object.values(pkgs) .filter( pkg => Object.keys(pkg.currentDependencies || {}).some( pkgId => pkgId === id, - ) && !emver.satisfies(version, pkg.currentDependencies[id].versionSpec), + ) && + !exver.satisfies(version, pkg.currentDependencies[id].versionRange), ) .map(pkg => getManifest(pkg).title) } diff --git a/web/projects/ui/src/manifest.webmanifest b/web/projects/ui/src/manifest.webmanifest index fee3469fc..cecc31bfe 100644 --- a/web/projects/ui/src/manifest.webmanifest +++ b/web/projects/ui/src/manifest.webmanifest @@ -5,8 +5,8 @@ "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", diff --git a/web/projects/ui/src/polyfills.ts b/web/projects/ui/src/polyfills.ts index 67caa24e8..813796e34 100644 --- a/web/projects/ui/src/polyfills.ts +++ b/web/projects/ui/src/polyfills.ts @@ -52,8 +52,8 @@ * */ -;(window as any).global = window -;(window as any).process = { env: { DEBUG: undefined }, browser: true } +(window as any).global = window +; (window as any).process = { env: { DEBUG: undefined }, browser: true } import { Buffer } from 'buffer' window.Buffer = Buffer